虚拟机类加载机制
2018-10-25
1. 类加载的时机
下面 4 种情况下,类将被初始化:
-
遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条指令的时,对应的类如果没有初始化,那么需要触发其初始化。4 条指令对应的 Java 代码场景是:
- 使用 new 关键字初始化对象的时候;
- 读取一个类的静态变量;
- 设置一个类的静态变量;
- 调用一个类的静态方法。
-
使用
java.lang.reflect
包的方法对类进行反射调用的时候,如果类没有初始化,那么需要触发其初始化。 -
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
对于接口而言,接口在初始化的时候,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
2. 类加载过程
-
加载
加载阶段,虚拟机做了 3 件事情:
- 通过一个类的 全限定名 来获取定义此类的二进制字节流(.class文件);
- 将这个字节流所代表的静态存储结构转化为 方法区 的运行时数据结构;
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。
但是数组的元素类型是由类加载器创建。
-
验证
连接阶段的第一步
目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
-
准备
连接阶段的第二步
目的是为类变量(被 static 修饰的变量)分配内存并设置类变量 初始值 的阶段,这些变量所使用的内存都将在方法区中进行分配。
这里面说到的初始值 “通常情况” 下是数据类型的 零值。
Java 基本数据类型的零值
数据类型 零值 int 0 long 0L short (short)0 char ‘\u0000’ byte (byte)0 boolean false float 0.0f double 0.0d reference null 注意:通常情况下,初始值是零值;但是在特殊情况下:如果类字段的字段属性表中存在 ConstantValue 属性,那么就会将其准备阶段的变量就会被初始化成 ConstantValue 的值。
-
解析
连接阶段的最后一步
目的是虚拟机将常量池内的符号引用替换为直接引用的过程。
-
符号引用:符号引用以一组符号来描述引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
-
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行解析。
-
-
初始化
类加载的最后阶段
这个阶段才真正开始执行类中定义的 Java 程序代码(或者是字节码)。
初始化阶段是执行类构造器
<clinit>()
方法的过程。-
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。 -
静态语句块只能访问到定义在静态语句块之前的变量。定义在静态语句块之后的变量,在前面的静态语句块中可以定义。
-
虚拟机会保证在子类的
<clinit>
()方法执行之前,父类的<clinit>
()方法已经执行完毕。这就意味着父类的静态语句块要优先于子类的静态语句块。 -
<clinit>
()方法对于类或者接口来说并不是必需的。因为如果类中既没有静态语句块,也没有静态变量赋值的赋值动作,那么编译器可以不为这个类生成<clinit>
()方法。 -
接口中不能使用静态语句块。接口与类都能生成
<clinit>
()方法,但是不一样,区别在于,接口在执行<clinit>
()方法时,不会先去执行父接口的<clinit>
()方法。只有等到需要的时候才会去执行父接口的<clinit>
()方法。 -
<clinit>
()方法在多线程的环境中能被JVM正确的加锁、同步。
-