一、概述
代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制。
二、类加载过程
以自定义的Math类为例,分析类加载全过程。实例代码如下:
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() { //一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
以上面Math类为例,类加载全过程流程如下
1、JVM加载过程分析
其中在JVM中通过类加载器ClassLoader的loadClass方法对类进行装载。loadClass方法加载过程有以下几步:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。
1.1 加载
所谓加载不是指的类加载机制,只是类加载机制中的第一步加载。这个阶段就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板调用Java类中的任意信息。创建的类模板对象的结构存储在方法区中。
一般来说加载分为以下几步:
a. 通过一个类的全限定名(包名与类名)获取此类的二进制字节流(Class文件)。
b. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)
c. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中
1.2 验证
验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。
验证作为链接的第一步,用于确保类或接口的二进制表示结构上是正确的,从而确保字节流包含的信息对虚拟机来说是安全的。
Java虚拟机规范中关于验证阶段的规则也是在不断增加的,但大体上会完成下面4个验证动作。
a. 文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理。
b. 元数据验证:主要对字节码描述的信息进行语义分析,以保证其提供的信息符合Java语言规范的要求。
c. 字节码验证:主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,字节码验证将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
d. 符号引用验证:最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段解析阶段发生。符号引用是对类自身以外(常量池中的各种符号引用)的信息进行匹配校验。
1.3 准备
给类或接口的静态变量分配内存空间,并且默认初始化这些字段。 并赋予默认值 如 int类型为0,String类型为null, boolean 类型为false等
1.4 解析
将常量池中的符号引用替换为直接引用过程。该阶段会把一些静态方法(符号引用 如main()方法)替换为执行数据所存内存的指针或句柄(直接引用),这就是所谓的静态链接过程(类加载期间完成)。动态链接实在程序运行期间完成的将符号引用替换为直接引用(如 实际使用的方法 math.compute())。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是接受的符号引用必须是一致的,因为符号引用的字面量形式已经明确定义在Class文件格式中。
直接引用(Direct References):直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么它一定已经存在于内存中了。
1.5 初始化
初始化是类加载的最后一步。除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才开始真正执行Java代码。
初始化主要工作就是对类的静态变量初始化为指定的值(如initData = 666)和执行静态代码块。
注意:主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
三、类加载时机
Java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:
a. 使用new创建类的实例或使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),以及使用invokestatic调用一个静态方法的时候,对应类必须进行过初始化。
b. 通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。
c. 当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。
d. 当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。
e. 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
注意:虚拟机规范使用了“有且只有”这个词描述,这五种情况被称为“主动引用”,除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”。
1、主动引用情况的示例
new 对象触发类加载
/**
* 分析JVM内存模型运行
*/
public class Math {
private final static int initData = 666;
private static User user = new User();
public int compute(){ //一个方法对应一个块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math m = new Math();
int result = m.compute();
System.out.println(result);
}
static {
System.out.println(initData);
}
}
运行结果
反射触发类加载
示例代码如下
public class ReflectTest {
static {
System.out.println("ReflectTest static block");
}
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("com.test.jvm.classloader.ReflectTest");
}
}
代码运行结果
加载子类会先加载父类
实例代码如下
public class ExtendTest {
public static void main(String[] args) {
System.out.println(new B().str);
}
//父类
static class A {
static {
System.out.println("A static block");
}
}
//子类
static class B extends A {
public String str = "B str";
static {
System.out.println("B static block");
}
}
}
示例运行结果
主类触发类加载
示例代码如下
public class HeapTest {
private final String str = "0987654321";
private final int data = 123456789;
public static void main(String[] args) throws InterruptedException {
System.out.println("验证堆内存溢出情况");
List<HeapTest> heapTestList = new ArrayList<HeapTest>();
while (true){
HeapTest heapTest = new HeapTest();
heapTestList.add(heapTest);
//Thread.sleep(10);
}
}
}
示例运行结果
2、被动引用情况的示例
子类引用父类静态字段
通过子类引用父类的静态字段,对于父类属于“主动引用”的第一种情况,对于子类,没有符合“主动引用”的情况,故子类不会进行初始化。代码如下
public class ExtendTest {
public static void main(String[] args) {
//System.out.println(new B().str);
System.out.println(B.value);
}
//父类
static class A {
//静态变量
public static int value = 666;
static {
System.out.println("A static block");
}
}
//子类
static class B extends A {
public String str = "B str";
static {
System.out.println("B static block");
}
}
}
示例运行结果
数组引用类
通过数组来引用类,不会触发类的初始化,因为是数组new,而类没有被new,所以没有触发任何“主动引用”条款,属于“被动引用”。代码如下:
//数组测试类
public class ArrayTest {
//静态变量value
public static int value = 666;
//静态块,父类初始化时会调用
static{
System.out.println("父类初始化!");
}
}
//主测试类
public class Test {
public static void main(String[] args) {
ArrayTest[] tests = new ArrayTest[10];
}
}
运行结果
引用静态变量
静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类,这是一个特例,需要特别记忆,不会触发类的初始化!
//静态变量测试类
public class ConstClass {
static{
System.out.println("常量类初始化!");
}
public static final String HELLOWORLD = "hello world!";
}
//主测试类
public class Test {
public static void main(String[] args) {
//ArrayTest[] tests = new ArrayTest[10];
System.out.println(ConstClass.HELLOWORLD);
}
}
运行结果