likes
comments
collection
share

JVM架构之类加载系统,深入理解类加载器

作者站长头像
站长
· 阅读数 20

JVM的整体架构

分为类加载,运行时数据区,执行引擎。这篇文章就先来讲讲类加载系统。

JVM架构之类加载系统,深入理解类加载器

类加载系统

类的加载,是生成class对象的过程。将class字节码信息加载进内存。类加载机制只负责class文件的加载,至于是否可以执行,则是由执行引擎决定。

类加载流程(必须掌握)🚩

从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接

1、加载

类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全限定名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

类的加载依赖于类加载器,后文会详细分析。

2、验证

检验二进制字节流是否符合JVM规范,比如魔数是否正确,版本号是否匹配等等。

3、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。只分配static修饰的变量的内存空间,且值为默认值,比如public static int value = 111,此时value = 0,也就是赋予数据类型的默认值。

在 JDK 7 之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。

特例:public static final int value = 111

4、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用:就是一组符号来描述目标,可以是任何字面量。

直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

  • 符号引用:符号引用本身可以是任何形式的字面量,它的实现取决于JVM,只要能定位到目标即可。 符号引用的目标不一定被加载到内存中
  • 直接引用:直接指向目标的引用。直接引用的目标一定在内存中存在

解析的时机

解析并不只出现在类加载过程,有些方法可能是真正被调用时才做解析操作。通常类加载时的解析包括了描述唯一版本的方法的符号引用,比如:final修饰的方法,版本唯一确定,就更适合在类加载阶段直接替换为直接引用。

《Java虚拟机规范》之中并未规定解析阶段发生的具体时间。只要求17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。比如:类加载时解析,或者符号引用将要被用才解析都是可以的。

对同一个符号引用进行多次解析请求是很常见的事情。除invokedynamic指令,其他解析请求的结果都可以被缓存。

invokedynamic:用于支持动态绑定

必须等到程序实际运行到这条指令时,解析动作才能进行。相对地,其余可触发解析的指令都是“静态”的

解析阶段是唯一一个顺序不确定的,在某些情况下解析可以在初始化阶段之后再开始,从而支持动态绑定。当然这里的类加载的顺序只是步骤开始的顺序,即可能存在一边加载,一边验证的情况,一个步骤的过程中激活了下一个步骤。

5、初始化

初始化阶段就是执行:类构造器 <clinit> ()方法的过程,它是Javac编译器的自动生成物

主要是对static修饰的字段进行赋值:

  • 声明类变量是指定初始值
  • 使用静态代码块为类变量指定初始值

当然如果父类没有被初始化,会先初始化其父类。

注意只针对static修饰的类变量。

6、使用

有了class对象,就可以在堆上为这个类分配实例对象,或者调用它的静态方法了。

7、卸载

卸载类即该类的 Class 对象被 GC。

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

没有实例对象,没有被引用,且类加载器还要被GC,因此类的class对象被GC是一件不太发生的事件。

只有自定义类加载器加载的类才有可能被卸载,JVM三个自带的类加载器加载的Class永远不会被卸载

类加载器🚩

类加载器只用于实现类的加载动作,但却用来标识类的唯一性。

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等。

Bootstrap ClassLoader

引导类加载器,也被叫做启动类加载器或根类加载器。C++实现。

引导类加载器只为JVM提供加载服务,开发者不能直接使用它来加载自己的类

负责加载:<JAVA_HOME>/lib目录下并且被虚拟机识别的类库

包括:java.util,java.io,java.lang下的类

Ext ClassLoader

源码在:sun.misc.Launcher$ExtClassLoader

加载<JAVA_HOME>\lib\ext目录下

包括:以javax开头的类,swing系列、内置的js引擎、xml解析器等

App ClassLoader

源码在:sun.misc.Launcher$AppClassLoader

加载系统类路径java -classpath-D java.class.path指定路径下的类库

我们自己编写的代码以及使用的第三方的jar包都是由它来加载的。

类加载器的加载

Bootstrap会负责加载Extension,并将Extension的父加载器设置为Bootstrap自己。

Bootstrap随后加载AppClassLoader,并将AppClassLoader的父加载器设置为Extension。

AppClassLoader负责加载自定义类加载器,并将自定义ClassLoader的父加载器设置为AppClassLoader。

注意:在获取ExtClassLoader的父类加载器时,获取到的结果为null,因为Bootstrap由C++实现。

Class类是按需加载的,使用到才触发加载。

命名空间:存储加载的类

每个类加载器都拥有一个自己的命名空间,用于存储被自身加载过的所有类的全限定名。子类加载器可以检查父类加载器中加载的类,通过拿类的全限定名在父类的命名空间内搜索匹配。但父类不可以看子类加载了哪些类

JVM如何判断两个类是否相同

通过ClassLoaderId + PackageName + ClassName进行判断。

这个非常重要,即类加载器的作用不仅仅有加载类,还用于区分类,为我们自定义类加载器的应用场景埋下伏笔。

双亲委派机制🚩

为什么需要双亲委派机制?

解决类加载器的隔离性问题,保证了 Java 程序的稳定运行

可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)

保证了 Java 的核心 API 不被篡改

流程如下图:

JVM架构之类加载系统,深入理解类加载器

总结:

  1. 自下而上,检查当前类加载器的命名空间内是否有这个类,即是否类已经被加载过
  2. 自上而下,尝试加载类;自己加载不了这个类,就交给子类来加载

如果加载失败,抛出ClassNotFoundException异常

注意:类加载器的父子关系是优先级,而不是指java继承中的父子关系。

双亲委派的实现原理

除了Bootstrap其他的所有类加载器都继承自ClassLoader

ClassLoader.loadClass方法,这个方法揭示了双亲委派机制

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 从自己的命名空间中查找该Class对象
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // 自己的命名空间中没找到Class
            try {
                if (parent != null) {
                    // 父加载器 加载
                    c = parent.loadClass(name, false);
                } else {
                    // Ext Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                // findClass方法在ClassLoader中的处理是直接抛出ClassNotFoundException异常
                // 是可扩展的一个方法 自定义处理
                c = findClass(name);
            }
        }
        // 是否需要解析
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

而ExtClassLoader没有重写loadClass()方法,AppClassLoader加载器调用父类的loadClass()方法,所以都没有打破双亲委派模型。

双亲委派的局限性

虽然避免了类的重复加载,但是当我们对类进行了修改,JVM是无法感知的,因为再次用到这个Class时,发现类加载器的命名空间内有了,就不会再次去尝试加载。此时我们必须重启整个项目。热部署就是解决这个问题的,而它的原理就是通过利用不同的类加载器,去加载更改后的class文件,从而在内存中创建出两个不同的Class对象。从而达到类文件更改后可以生效的目的。

自定义类加载器

一般继承ClassLoader,或是URLClassLoader ,重写方法即可。

如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

自定义类加载器的场景

类加载器实战

推荐李号双的《深入拆解Tomcat & Jetty》有详细分析Tomcat实现的类加载器,《深入理解Java虚拟机 第三版》也写的很好

参考文献

周志明 《深入理解Java虚拟机 第三版》