虚拟机类加载机制概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
Java与编译时需要进行连接工作的语言(C、C++)不同,Java中,类型的加载、连接和初始化过程都是在程序运行期间完成。
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析部分称为连接。
虚拟机规范严格规定有且只有5种情况必须立即对类进行“初始化”:
- 遇到new、getstatic、putstatic或invokestatic指令
- 使用java.lang.reflect包的方法对类进行反射调用
- 初始化一个类,其父类未进行初始化,则先触发其父类的初始化
- 虚拟机启动时,指定一个主类(包含main()方法的那个类)会被初始化
- 使用java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,则方法句柄对应的类触发初始化
注:已经初始化的类不会再次初始化
以下场景不会触发类的初始化
- 引用父类的静态属性
public class SuperClassTest {
public static String name = "SuperClassTest";
static {
System.out.println("init SuperClassTest");
}
}
public class SubClassTest extends SuperClassTest {
static {
System.out.println("init SubClassTest");
}
}
public class InitClassTest {
public static void main(String[] args) {
System.out.println(SubClassTest.name);
}
}
执行结果:
init SuperClassTest
SuperClassTest
可以看到,引用父类的静态属性,该类是不会触发初始化。
- 通过数组定义引用类
public class SuperClassTest {
public static String name = "SuperClassTest";
static {
System.out.println("init SuperClassTest");
}
}
public class InitClassTest {
public static void main(String[] args) {
SuperClassTest[] tests = new SuperClassTest[8];
}
}
执行结果没有输出init SuperClassTest,说明SuperClassTest类并未被初始化。
- 使用类的常量
public class SuperClassTest {
public final static String name = "SuperClassTest";
static {
System.out.println("init SuperClassTest");
}
}
public class InitClassTest {
public static void main(String[] args) {
System.out.println(SuperClassTest.name);
}
}
执行结果:
SuperClassTest
说明,调用类的常量,final static属性,并不会触发类的初始化。
类加载的过程
指加载、验证、准备、解析、初始化5个阶段。
-
加载
注意,加载指的是类加载
在加载阶段,虚拟机需要完成以下3件事情:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 生成一个代表该类的Class对象,作为方法区该类的各种数据访问入口
二进制字节流获取途径:
- ZIP、JAR、EAR、WAR包
- 网络中获取,类似Applet
- 动态代理获取,java.lang.reflect
- Proxy中获取,使用ProxyGenerator.generateProxyClass为特定接口生成“$Proxy”的代理类的二进制字节流
- 从数据库中获取
......
-
验证
验证是连接阶段的第一步,该阶段目的是确保Class文件的字节流中包含信息符合当前虚拟机的要求。
Class文件的二进制流是可以修改的,假设没有验证环节,执行被修改的Class文件后果是不可预知的。
试验一下,当前目录创建Test.java
public class Test {
public static void main(String[] args) {
System.out.println("hello");
}
}
编译,正常运行,会输出hello
$ javac Test.java
$ java Test
hello
vim -b 打开Test.class文件
$ vim -b Test.class
<ca><fe><ba><be>^@^@^@4^@^]
^@^F^@^O ^@^P^@^Q^H^@^R
^@^S^@^T^G^@^U^G^@^V^A^@^F<init>^A^@^C()V^A^@^DCode^A^@^OLineNumberTable^A^@^Dmain^A^@^V([Ljava/lang/String;)V^A^@
SourceFile^A^@ Test.java^L^@^G^@^H^G^@^W^L^@^X^@^Y^A^@^Ehello^G^@^Z^L^@^[^@^\^A^@^DTest^A^@^Pjava/lang/Object^A^@^Pjava/lang/System^A^@^Cout^A^@^ULjava/io/PrintStream;^A^@^Sjava/io/PrintStream^A^@^Gprintln^A^@^U(Ljava/lang/String;)V^@!^@^E^@^F^@^@^@^@^@^B^@^A^@^G^@^H^@^A^@ ^@^@^@^]^@^A^@^A^@^@^@^E*<b7>^@^A<b1>^@^@^@^A^@
^@^@^@^F^@^A^@^@^@^A^@ ^@^K^@^L^@^A^@ ^@^@^@%^@^B^@^A^@^@^@ <b2>^@^B^R^C<b6>^@^D<b1>^@^@^@^A^@
^@^@^@
^@^B^@^@^@^C^@^H^@^D^@^A^@^M^@^@^@^B^@^N
命令模式下输入“:%!xxd”,得出十六进制内容,下面显示前面部分内容
00000000: cafe babe 0000 0034 001d 0a00 0600 0f09 .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507 ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
把第二行的0010 0011改成0010 0012
00000000: cafe babe 0000 0034 001d 0a00 0600 0f09 .......4........
00000010: 0010 0012 0800 120a 0013 0014 0700 1507 ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
命令模式下输入“:%!xxd -r”,还原回来,并保持退出“:wq”,再运行java Test
$ java Test
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Invalid constant pool index 18 in class file Test
at java.lang.ClassLoader.defineClass1(Native Method)
发现执行结果会抛出异常,以同样的步骤,将0010 0012修改回0010 0011,执行java Test,发现执行结果正常。
其实,抛出异常就是在验证阶段进行的。
整体上,验证有4个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
-
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,变量所使用的内存都将在方法区中进行分配。
- 通常情况下,基本数据类型的类变量初始值为零值
private static int test = 123;
test值初始化时首先为0,直到执行putstatic指令,test被赋值为123,该指令存放于类构造器<clinit>()方法中。
基本数据类型的零值表
数据类型 | 零值 | 数据类型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte)0 |
- 基本数据类型常量初始值为该常量
private static final int test = 123;
在准备阶段时,如果字段属性表中存在ConstantValue属性,准备阶段时,test被赋值为该ConstantValue属性。
-
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用和直接引用
- 符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量。与虚拟机实现的内存布局无关。 - 直接引用
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接锁定位到目标的句柄。与虚拟机实现的内存布局相关。
解析动作主要针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
-
初始化
类初始化阶段是类加载过程的最后一步,初始化阶段会执行类中定义的Java代码。
- <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})
- <clinit>()方法与类的构造函数不同,它不需要显式调用父类构造函数,虚拟机保证子类<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。
- 类与接口<clinit>()方法不是必须的。
- 接口不能使用静态语句块,但可以有变量初始化赋值操作。执行接口<clinit>()方法前不需要先执行父接口<clinit>()方法。
- 虚拟机保证一个类的<clinit>()方法线程安全。
网友评论