JVM类加载过程共分为加载、验证、准备、解析、初始化、使用和卸载七个阶段
这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
加载
加载过程是JVM类加载的第一步,如果JVM配置中打开-XX:+TraceClassLoading
,我们可以在控制台观察到类似
[Loaded chapter7.SubClass from file:/E:/EclipseData-Mine/Jvm/build/classes/]
的输出,这就是类加载过程的日志。
加载过程是作为程序猿最可控的一个阶段,因为你可以随意指定类加载器,甚至可以重写loadClass
方法,当然,在jdk1.2及以后的版本中,loadClass
方法是包含双亲委派模型的逻辑代码的,所以不建议重写这个方法,而是鼓励重写findClass
方法。
类加载的二进制字节码文件可以来自jar
包、网络、数据库以及各种语言的编译器编译而来的.class
文件等各种来源。
加载过程主要完成如下三件工作:
1>通过类的全限定名(包名+类名)来获取定义此类的二进制字节流
2>将字节流所代表的静态存储结构转化为运行时数据结构存储在方法区
3>为类生成java.lang.Class
对象,并作为该类的唯一入口
这里涉及到一个概念就是类的唯一性,书上对该概念的解释是:在类的加载过程中,一个类由类加载器和类本身唯一确定。也就是说,如果一个JVM虚拟机中有多个不同加载器,即使他们加载同一个类文件,那得到的java.lang.Class
对象也是不同的。因此,只有在同一个加载器中,一个类才能被唯一标识,这叫做类加载器隔离。
验证
验证过程相对来说就有复杂一点了,不过验证过程对JVM的安全还是至关重要的,毕竟你不知道比人的代码究竟能干出些什么。
验证过程主要包含四个验证过程:
1>文件格式验证
四个验证过程中,只有格式验证是建立在二进制字节流的基础上的。格式验证就是对文件是否是0xCAFEBABE
开头、class文件版本等信息进行验证,确保其符合JVM虚拟机规范。
2>元数据验证
元数据验证是对源码语义分析的过程,验证的是子类继承的父类是否是final类;如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法;子父类中的字段、方法是否产生冲突等,这个过程把类、字段和方法看做组成类的一个个元数据,然后根据JVM规范,对这些元数据之间的关系进行验证。所以,元数据验证阶段并未深入到方法体内。
3>字节码验证
既然元数据验证并未深入到方法体内部,那么到了字节码验证过程,这一步就不可避免了。字节码主要是对方法体内部的代码的前后逻辑、关系的校验,例如:字节码是否执行到了方法体以外、类型转换是否合理等。
当然,这很复杂。
所以,即使是到了如今jdk1.8,也还是无法完全保证字节码验证准确无遗漏的。而且,如果在字节码验证浪费了大量的资源,似乎也有些得不偿失。
4>符号引用验证
符号引用的验证其实是发生在符号引用向直接引用转化的过程中,而这一过程发生在解析阶段。
因为都是验证,所以一并在这讲。符号引用验证做的工作主要是验证字段、类方法以及接口方法的访问权限、根据类的全限定名是否能定位到该类等。具体过程会在接下来的解析阶段进行分析。
好了,验证阶段的工作基本就是以上四类,下面我们来看下一个阶段。
准备
相信经历过艰辛的验证阶段的磨练,JVM和我们都倍感疲惫。所以,接下来的准备阶段给我们提供了一个相对轻松的休息阶段。
准备阶段要做的工作很简单,他瞄准了类变量这个元数据,把他放进了方法区并进行了初始化,这里的初始化并不是<init>
或者<clinit>
操作,准备阶段只是将这些可爱的类变量置零。
解析
这一部分我画了几个图,内容有些多,放在另一篇文章里:解析
初始化
初始化阶段是我们可以大搞实验的一块实验田。首先,初始化阶段做什么?这个阶段就是执行<clinit>
方法。而<clinit>
方法是由编译器按照源码顺序依次扫描类变量的赋值动作和static
代码块得到的。
那么问题来了,啥时候才会触发一个类的初始化的操作呢?答案有且只有五个:
1>在类没有进行过初始化的前提下,当执行new
、getStatic
、setStatic
、invokeStatic
字节码指令时,类会立即初始化。对应的java操作就是new
一个对象、读取/写入一个类变量(非final
类型)或者执行静态方法。
2>在类没有进行过初始化的前提下,当一个类的子类被初始化之前,该父类会立即初始化。
3>在类没有进行过初始化的前提下,当包含main
方法时,该类会第一个初始化。
4>在类没有进行过初始化的前提下,当使用java.lang.reflect
包的方法对类进行反射调用时,该类会立即初始化。
5>在类没有进行过初始化的前提下,当使用JDK1.5
支持时,如果一个java.langl.incoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
以上五种情况被称作类的五种主动引用,除此之外的任何情况都被相应地叫做被动引用。以下是集中常见的且容易迷惑人心智的被动引用的示例:
/**
通过子类引用父类的类变量不会触发子类的初始化操作
*/
public class SuperClass {
public static String value = "superClass value";
static {
System.out.println("SuperClass init!");
}
}
public class SubClass extends SuperClass implements SuperInter{
static {
System.out.println("SubClass init!");
}
}
public class InitTest {
static {
System.out.println("InitTest init!");//main第一个初始化
}
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
/**
output:
InitTest init!
SuperClass init!
superClass value
*/
/**
通过定义对象数组的方式是不能触发对象初始化的
*/
public static void main(String[] args) {
SubClass[] superArr = new SubClass[10];
}
/**
output:
InitTest init!
*/
/**
引用类的final类型的类变量无法触发类的初始化操作
*/
public class SuperClass {
public static final String CONSTANT_STRING = "constant";
static {
System.out.println("SuperClass init!");
}
}
public class InitTest {
static {
System.out.println("InitTest init!");//main
}
public static void main(String[] args) {
System.out.println(SuperClass.CONSTANT_STRING);//getStatic
}
}
/**
output:
InitTest init!
constant
*/
了解了什么时候出发初始化操作后,那么初始化操作的执行顺序是什么样的?并发初始化情况下的运行机制又如何?
JVM虚拟机规定了几条标准:
- 先父类后子类,(源码中)先出现先执行
- 向前引用:一个类变量在定义前可以赋值,但是不能访问。
- 非必须:如果一个类或接口没有类变量的赋值动作和
static
代码块,那就不生成<clinit>
方法. - 执行接口的
<clinit>
方法不需要先执行父接口的<clinit>
方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>
方法。 - 同步性:
<clinit>
方法的执行具有同步性,并且只执行一次。但当一个线程执行该类的<clinit>
方法时,其他的初始化线程需阻塞等待。
我们通过一个实例来验证线程的阻塞问题:
public class SuperClass {
static {
System.out.println("SuperClass init!");
System.out.println("Thread.currentThread(): " + Thread.currentThread() + " excuting...");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class InitTest {
static {
System.out.println("InitTest init!");//main
}
public static void main(String[] args) throws ClassNotFoundException, InterruptedException {
currentInitTest();
}
public static void currentInitTest() throws InterruptedException {
Runnable run = new Runnable() {
@Override
public void run() {
System.out.println("Thread.currentThread(): " + Thread.currentThread() + " start");
new SuperClass();
System.out.println("Thread.currentThread(): " + Thread.currentThread() + " end");
}
};
Thread[] threadArr = new Thread[10];
for (int i = 0; i < 10; i++) {
threadArr[i] = new Thread(run);
}
for (Thread thread : threadArr) {
thread.start();
}
}
}
/**
output:
InitTest init!
Thread.currentThread(): Thread[Thread-0,5,main] start
Thread.currentThread(): Thread[Thread-1,5,main] start
Thread.currentThread(): Thread[Thread-2,5,main] start
Thread.currentThread(): Thread[Thread-7,5,main] start
Thread.currentThread(): Thread[Thread-6,5,main] start
Thread.currentThread(): Thread[Thread-3,5,main] start
Thread.currentThread(): Thread[Thread-5,5,main] start
Thread.currentThread(): Thread[Thread-9,5,main] start
Thread.currentThread(): Thread[Thread-4,5,main] start
Thread.currentThread(): Thread[Thread-8,5,main] start
SuperClass init!
Thread.currentThread(): Thread[Thread-0,5,main] excuting...
Thread.currentThread(): Thread[Thread-9,5,main] end
Thread.currentThread(): Thread[Thread-3,5,main] end
Thread.currentThread(): Thread[Thread-6,5,main] end
Thread.currentThread(): Thread[Thread-7,5,main] end
Thread.currentThread(): Thread[Thread-0,5,main] end
Thread.currentThread(): Thread[Thread-5,5,main] end
Thread.currentThread(): Thread[Thread-4,5,main] end
Thread.currentThread(): Thread[Thread-8,5,main] end
Thread.currentThread(): Thread[Thread-1,5,main] end
Thread.currentThread(): Thread[Thread-2,5,main] end
*/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。