JVM-Class装载系统
标签:JVM

Class装载系统

1. 装载流程

系统装载Class文件分为加载,连接,和初始化,其中连接过程又分为验证,准备和解析

2. 类装载的条件

Class文件只有在必须要使用的时候才会被加载,JVM不会无条件的装载Class类型。Java虚拟机规定:一个类或接口在初次使用前,必须要进行初始化。这里的使用值的是主动使用,包括:

  1. 当创建一个类的实例时,比如使用new关键字,或者通过反射,克隆,反序列化。
  2. 当调用类的静态方法时,即当使用了字节码invokestatic指令。
  3. 当使用类或接口的静态字段时(final常量除外),比如,使用getstatic或者putstatic指令。
  4. 当使用java.lang.reflect包中的方法反射类的方法时。
  5. 当初始化子类时,要求先初始化父类。
  6. 最为启动虚拟机,含有main()方法的那个类。

除以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。


被动引用一:

父类中有静态变量v,在main函数中,使用子类去调用父类的v,可见,上面虽然访问了子类对象,但子类并未被初始化,只有父类被初始化。**可见,在引用一个字段时,只有直接定义该字段的类,才会被初始化。**但是,虽然Child类没有被初始化,但是,此时Child类已经被系统加载,只是没有进入初始化阶段。使用 -XX:+TraceClassLoading参数可以查看到。


被动引用二:

上面的FinalFieldClass并没有因为其常量字段constString被引用而被初始化,这是因为在Class文件生成时,final常量由于其不变性,做了适当的优化。直接将其final常量直接存放到常量池中,因此,FinalFieldClass类自然不会被加载。在上的类加载日志就没有FinalFieldClass类出现。

故并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会被初始化。

3. 加载类

加载类的时候要完成以下工作:

  1. 通过类的全名,获取类的二进制数据流。
  2. 解析类的二进制数据流为方法区内的数据结构。
  3. 创建java.lang.Class类的实例,表示该类型。

对于获取类的二进制数据流,虚拟机可以通过多种途径产生或获得,可以通过class文件,jar包,zip等。获取到类的二进制信息后,JVM就会处理这些数据,并转化为一个Class的实例,通过Class类提供的接口,可以访问一个类型的方法,字段等信息。具体参考:Java基础-反射

4. 验证类

验证类主要分为:格式检查,语义检查,字节码验证,符号引用验证。

  1. 必须判断类的二进制数据是否是符合格式要求和规范的。
    1. 如魔数0xCAFEBABE
    2. 主版本和小版本是否在当前Java虚拟机支持的范围内
    3. 数据中的每一项是否拥有正确的长度
  2. 进行字节码语义检查
    1. 是否所有的类都有父类存在(在Java中,除了Object外,其他类都应该由父类存在)
    2. 是否一些被定义为final的方法或者类被重载或继承了。
    3. 非抽象类是否实现了所有抽象方法或者接口方法
    4. 是否存在不兼容的方法(比如方法签名除了返回值不同,其他都一样,这种会使虚拟机无从下手)
  3. 进行字节码验证,最复杂,试图通过对字节码流的分析,判断字节码是否可以被正确执行,通过该阶段也不能100%保证这个类是完全正确的。
    1. 比如字节码的执行过程中,是否会跳到一条不存在的指令
    2. 函数的调用是否传递了正确类型的参数
    3. 变量的赋值是否给出了正确的数据类型
  4. 进行符号引用的验证,Class文件在其常量池会通过字符串记录自己要使用的其他类或方法。故在验证阶段就会去检查这些类或方法是否存在,判断当前类是否有权限去访问这些数据,如果一个类无法在系统中找到,则会抛出 NoClassDefFoundError,如果一个方法无法被找到,则会抛出 NoSuchMethodError

5. 准备

当一个类验证通过后,虚拟机就会进入准备阶段,在这个阶段,虚拟就就会为这个类分配相应的内存空间,并设置初始值。

类型 默认初始值
int 0
long 0L
short (short)0
char \u0000
boolean false
reference null
float 0f
double 0f

特别注意上面的char和short,对于boolean,java并不支持,内部实现是int,由于int默认值是0,故对应的,boolean默认值就是false

如果存在常量字段,那么常量也会在准备阶段赋上正确的值,这个赋值属于Java虚拟机的行为,属于变量初始化,在准备阶段,不会有任何Java代码被执行

6. 解析类

在准备阶段完成以后,就进入到解析阶段,解析阶段就是将类,接口,字段和方法的符号引用转为直接引用。

对于方法,JVM为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使的方法被成功调用。

故,解析就是将符号引用转为直接引用,也就是得到类或字段,方法在内存中的指针或者偏移量,因此,如果直接引用存在,那么可以肯定系统中存在该类,方法或者字段。但只存在符号引用,不能确定系统中一定存在该对象。


CONSTANT_String的解析:

当在Java代码中直接使用字符串常量时,就会在类中出现CONSTANT_String,它表示字符串常量,并且会引用一个CONSTANT_UTF8的常量项。在JVM虚拟机内部运行时的常量池中,会维护一张字符串拘留表,他不会保存所有出现过的字符串常量,并且没有重复项。只要以CONSTANT_UTF8形式出现的字符串都会在这张表中。使用 String.intern()方法可以得到一个字符串在拘留表(intern)中的引用,因为该表中,没有重复项,所以任何字面量相同的字符串的 String.intern()方法返回总是相等的。 具体可以查看:Java基础-String 在虚拟机中的实现

7. 初始化

类的初始化是类装载的最后一个阶段,如果前面的都没有问题的话,那么类就可以顺利的装载到系统中。此时,类才会开始执行Java字节码。初始化阶段的工作时执行类的初始化方法 <clinit> ,方法 <clinit> 是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句合并产生的。

由于执行子类的初始化方法前会先去执行父类的初始化方法,故子类的static块的优先级比父类的static块的优先级高。

注意如果一个类既没有赋值语句,也没有static语句块,那么生成的 <clinit> 函数就为空,如,类中只有final变量,在准备阶段就完成了初始化,那么就不会存在该 类的<clinit> 方法。

对于 <clinit> 函数的调用,也就是类的初始化,虚拟机会在内部确保多线程环境下的安全性,也就是说,**当多个线程试图初始化同一个类的时候,只有一个线程可以进入 <clinit> 方法,其他线程必须等待,如果前面的线程成功加载了类,后面的队列中的线程就不会执行 <clinit> 方法了,而是直接返回准备好的信息。**由于 <clinit> 是线程安全的,因此在多线程的环境下,可能会导致死锁的发生,而且很难察觉到。即两个线程同时初始化各自的类,但各自的类中有尝试去初始化对方的类,这样就导致了环的形成,导致死锁。

  • 9 min read

CONTRIBUTORS


  • 9 min read