【jvm系列-02】jvm的类加载子系统以及jclasslib的基本使用

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【jvm系列-02】jvm的类加载子系统以及jclasslib的基本使用

1,jvm的内存结构

在jvm的内存中结构中,其主要结构如下。

在jvm内部,需要将磁盘上的字节码文件通过这个类加载加载到内存中。在类加载子系统中,也需要经过一定的阶段将才能将这个文件加载到内存的运行时数据区中,如一些加载,验证,准备,解析,初始化等工作。在加载到运行时数据区之后,内部主要由一些共享的方法区、堆,以及私有的程序计数器、虚拟机栈、本地方法栈这些。这些字节码最终是需要通过执行引擎去执行的,执行引擎中主要包括解释器,JIT即时编译器,垃圾回收器等。


2,类加载器加载过程

在类加载器子系统中,主要会经过加载,链接和初始化三个阶段,链接又包括验证,准备和解析三个阶段,所以合起来就是加载,验证,准备,解析,初始化五个阶段。


fd1dc67a0fa24ac0840c26bb3578874b.png


类加载器主要负责从文件系统或者网络中加载Class文件,并且类加载器只负责将文件加载,至于是否可以运行,还得由Execution Engine执行引擎决定。


2.1,加载阶段

加载阶段的加载器主要有引导类加载器,扩展类加载器,系统类加载器和自定义类加载器,主要是通过一个类的全限定名获取此类的二进制字节流,然后将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构,然后在内存中生成一个java.lang.Class文件,作为方法区这个类的各种数据的访问入口。其主要就是将文件加载出来


常见的类加载方式有以下几种方式


从本地系统直接加载

从网络中获取

从压缩包中获取,如zip

运行时生成,如动态代理

其他文件生成,典型的场景有:JSP应用

数据库中获取 .class文件

从加密文件中获取

反射,序列化,克隆等

2.2,链接阶段

链接阶段又可以分为三个阶段,分别是验证,准备和解析


2.2.1,验证

验证的主要目的在于确保Class文件的字节流中所包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全,相当于一种自我保护。如果编译器发现了有违法的信息之后,则编译器可以选择直接抛出异常或者拒绝编译。


主要包括四种验证:文件格式验证,元数据验证,字节码验证和符号引用验证。


2.2.2,准备

在准备阶段为类分配内存,并且设置该类的变量默认初始值,如整型的初始值为0。

public static int x = 10;  //在准备阶段赋值默认值为0,并且分配内存
public static void main(String[] args) {
    System.out.println(j);
}

这里主要是为变量进行一个默认的初始赋值,如果变量被static final修饰,那么这个变量会被变为常量,并且会在编译阶段就会分配内存。同时这里也不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。


2.2.3,解析

就是将常量池内的符号引用转化为直接引用的过程,并随着JVM在执行完初始化之后再执行。


符号引用:以一组符号来描述引用的目标,只要能无歧义的定位到目标即可

直接引用: 相当于寻址的直接指针或者句柄


解析动作主要针对接口,类,字段,类方法,接口方法,方法类型,句柄和调用点限定符


2.3,初始化阶段(重点)

2.3.1,jclasslib的安装

在查看字节码文件之前,也可以在idea中安装查看对应的字节码指令的插件,在插件中搜索jclasslib即可,安装完成之后需要restart重启。

e27658c94f214c5bb3eb9d5fdc1f9158.png



在安装完成之后,可以在view的位置来打开这个Bytecode字节码文件。


a26996b95b62444cb8a0597ff8f613b4.png


在点击这个Show Bytecode With Jclasslib 之后,就会出现以下的界面,会有一些版本,协议号,当前类,父类,接口数,文件数,方法数,属性数等。


039bdc91f1494047a7a1977012fe00e0.png

2.3.2,clinit

初始化阶段就是执行类构造器方法()的过程,通过javac编译器自动收集类中的所有类变量赋值动作和静态代码块中的语句合并而来的。就是说这个clinit会将类变量的显示的初始化和静态代码块的初始化合并到一起,如果没有类变量的赋值操作或者静态代码块的赋值操作,那么这个clinit就不会出现在字节码文件中。


并且在整个流程中,变量的初始赋值是在这个准备阶段,而真正的赋值是在这个初始化阶段。

public static int x = 10;  //当前阶段中此时x的值为10

在这个Methods中,可以看到给这个类变量赋值,是有这个clinit的


2729dc78fc6b444e8523246d5e47ca6d.png


或者再静态代码块中给类变量赋值,也是可以有这个clinit的,可以看下图右边Methods中的第二点。


1f11aaab6bd24aebb0859332cc18ff18.png


如果该类具有父类,那么JVM会保证先加载父类的 ,再加载子类的 。并且在多线程中,虚拟机会保证一个类的 方法会加同步锁


2.3.3,init

在每个类中,都会有一个隐示的构造方法或者显示的构造方法,通过 来进行初始化。如在以下的代码中,显示的写了一个代码的构造器,先将初始值加载,或者再加载构造器里面的值。



1764f69c06f841079debf480b5b27aaf.png

3,类加载器

3.1,类加载器的分类

在加载阶段中,主要有引导类加载器,扩展类加载器,应用程序类加载器和自定义加载器。在jvm中,规定支持两种类加载器,分別是引导类加载器和自定义类加载器,而扩展类和系统类都是属于自定义类加载器。


62e288ba64944d5dbb0aeae281f24f3f.png


并且在这几个来加载器中,这个引导类加载器是用c语言写的,而其他的类加载器都是使用这个JAVA语言写的。接下来通过代码查看一下这个类加载器,也可以发现这个引导类加载器不是java语言写的,所以获取不到,并且这个自定义类的加载器是通过系统类加载器来加载的。

//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(systemClassLoader);
//获取上层扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
//sun.misc.Launcher$ExtClassLoader@15615099
System.out.println(extClassLoader);
//获取上层引导类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
//null 尝试获取失败,该类由c语言编写
System.out.println(bootStrapClassLoader);
//获取自定义类类加载器,以当前类为例
ClassLoader classLoader = ClassLoad.class.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2 
// 可以发现当前自定义类的了地价在其为系统类加载器
System.out.println(classLoader);

而像一些系统的核心类库,如String这种,是通过引导类加载器加载的。并且该加载器作为扩展类和系统类加载器的父类加载器,该加载器主要加载包名为java,javax,sun等开头的类

ClassLoader StringClassLoader = String.class.getClassLoader();
System.out.println(StringClassLoader);  //null

接下来可以获取一下这个引导类中,加载的全部内容

//获取引导类加载器可以加载的全部url
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urLs.length; i++) {
    System.out.println(urLs[i]);
}

3.2,自定义类加载器的场景

一般情况使用引导类,扩展类和系统类是可以满足日常的开发需求的,但是在必要时,也可以手动自定义其他的类加载器。


引入自定义类加载器的原因


隔离加载类

修改类加载方式

扩展加载源

防止源码泄漏

自定义类加载器的实现步骤


1,可以通过继承抽象类 java.lang.ClassLoader 类,实现自定义类加载器

2,重写findClass()方法,然后将逻辑写在方法内部

3,如果没有特别复杂的要求,可以直接继承URLClassLoader类

获取ClassLoader的途径


获取当前类的ClassLoader:clazz.getClassLoader()

获取上下文线程方式:Thread.currentThread.getContextClassLoader()

获取系统的ClassLoader:ClassLoader.getSystemClassLoader()

获取调用者的ClassLoader:DriverManager.getCallerClassLoader()

3.3,双亲委派机制

在jvm中,对class文件采用的是按需加载的方式,也就是说在需要使用到该类时才会加载,然后将class文件加载到内存生成class对象。并且java虚拟机采用的是一种双亲委派机制模式


其工作原理如下:


1,如果一个类加载器收到了加载请求,他并不会自己去加载,而是将这个请求委托给父类加载器去执行

2,如果父加载器还有其他的父加载器,那么会进一步的向上委托,一次递归到顶点

3,如果父类可以完成任务,则将值返回;反之,则由子类尝试去加载

19811af50aeb4657ae626090fe3761b6.png

其源码如下

// 检查当前类加载器是否已经加载了该类
Class<?> c = findLoadedClass(name);
    if (c == null) {
        long t0 = System.nanoTime();
        try {
            if (parent != null) {  //如果当前加载器父加载器不为空则委托父加载器加载该类
                c = parent.loadClass(name, false);
            } else {  //如果当前加载器父加载器为空则委托引导类加载器加载该类
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }
    if (c == null) {
        // If still not found, then invoke findClass in order
        // to find the class.
        long t1 = System.nanoTime();
        //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
        c = findClass(name);

通过源码也可以知道:

1,首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。


2,如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。


3,如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。ClassLoader的loadClass方法,里面实现了双亲委派机制。


双亲委派机制的好处


1,沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改

2,避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性


3.4,自定义类加载器

需要继承ClassLoader类,并且重写里面的findClass()方法。可以在本地磁盘里面创建一个类,如何加载的时候直接通过本地磁盘加载,而不需要使用到那几个类加载器加载,这样就完成了自定义类的加载器

import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
 * @Author: zhenghuisheng
 * @Date: 2023/3/16 23:09
 */
public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader {
    private String classPath;
    public MyClassLoader(String classPath) {
            this.classPath = classPath;
    }
    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
             byte[] data = loadByte(name);
             //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
             return defineClass(name, data, 0, data.length);
             } catch (Exception e) {
             e.printStackTrace();
             throw new ClassNotFoundException();
            }
        }
    }
    /**
     * 下面的磁盘路径需要手动创建
     * @param args
     * @throws Exception
     */
    public static void main(String args[]) throws Exception {
        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //D盘创建 test/com/zhenghuisheng/jvm 几级目录,将User类的复制类User.class丢入该目录
        //需要创建一个User类在这个路径下
        Class clazz = classLoader.loadClass("com.zhenghuisheng.jvm.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

3.5,打破双亲委派模型

应用程序里面有这个类,磁盘里面也有这个自定义的类,直接通过磁盘一次性加载,不需要利用到父类加载器,这样就打破了双亲委派机制。主要就是通过重写里面的findClass()方法,将里面的双亲委派机制的逻辑修改即可。让这个findClass直接找磁盘里面的路径,而不需要再写那些层层找父加载器加载即可。


3.6,其他

JVM中的两个class对象是否为同一个类


类的完整名必须一致

加载这个类的ClassLoader必须相同

在jvm中,即使这两个对象来源于同一个Class文件,被同一个虚拟机所加载,但只要加载他们的ClassLoader实例对象不一致,那么这两个类对象也是不相等的。

相关文章
|
1月前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
44 3
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
43 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
2月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
112 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
3月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
27 3
|
3月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
181 0
|
4月前
|
存储 缓存 自然语言处理
(三)JVM成神路之全面详解执行引擎子系统、JIT即时编译原理与分派实现
执行引擎子系统是JVM的重要组成部分之一,在JVM系列的开篇曾提到:JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。
|
4月前
|
存储 前端开发 Java
(二)JVM成神路之剖析Java类加载子系统、双亲委派机制及线程上下文类加载器
上篇《初识Java虚拟机》文章中曾提及到:我们所编写的Java代码经过编译之后,会生成对应的class字节码文件,而在程序启动时会通过类加载子系统将这些字节码文件先装载进内存,然后再交由执行引擎执行。本文中则会对Java虚拟机的类加载机制以及执行引擎进行全面分析。
|
4月前
|
Java Perl
JVM内存问题之如何统计在JVM的类加载中,每一个类的实例数量,并按照数量降序排列
JVM内存问题之如何统计在JVM的类加载中,每一个类的实例数量,并按照数量降序排列
|
4月前
|
存储 安全 Java
开发与运维引用问题之JVM类加载过程如何解决
开发与运维引用问题之JVM类加载过程如何解决
31 0
|
4月前
|
存储 算法 Java
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决