JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,形成可以被虚拟机使用的Java类型,称作类加载机制,过程如下
加载、验证、准备、初始化和卸载这五个阶段的开始顺序是确定的,但可交叉运行
解析阶段在某些情况下可以在初始化阶段之后再开始,目的是为了实现动态绑定
什么时候开始加载并无强制约束,但在以下情况必须立即对类进行初始化
除主动引用外的所有引用类型方式都不会触发初始化
对于如下程序
public class Test {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
class SuperClass{
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static {
System.out.println("SubClass init");
}
}
打印
SuperClass init
123
对于静态字段,只有直接定义这个字段的类才会被初始化,由子类来引用父类中的静态字段,只会触发父类的初始化而不会触发子类的初始化
如下程序无打印
public class Test {
public static void main(String[] args) {
SuperClass[] sClasses = new SuperClass[10];
}
}
class SuperClass{
static {
System.out.println("SuperClass init");
}
}
没有触发SuperClass初始化,但newarray指令触发了[SuperClass初始化,它是由JVM生成、继承于Object的子类,代表一个元素类型为SuperClass的一维数组,其属性(如length)和方法(如clone)都在这个类中实现
对于如下程序
public class Test {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO_WORLD);
}
}
class ConstClass{
static {
System.out.println("ConstClass init");
}
public static final String HELLO_WORLD = "helloword";
}
打印
helloword
不会触发初始化,编译阶段会通过常量传播优化,将"helloword"字符串存储在Test.class,而不必通过ConstClass类去引用
通过类全限定名获取其二进制字节流,可通过zip、网络、数据库等方式
加载结束后,二进制字节流按照虚拟机指定的格式存储在方法区
堆中实例化一个Class对象,作为程序访问方法区中数据的外部接口
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的约束要求,作为代码运行后不会危害虚拟机自身的安全
验证字节流是否符合Class文件格式规范,且能被当前版本的虚拟机处理
上述验证阶段基于二进制字节流进行,通过之后存储到方法区,而后面三个验证阶段则基于方法区上进行,不会再读取、操作字节流
对元数据信息(数据类型)进行语义分析,保证符合《Java语言规范》
通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
对类的方法体(Code属性)进行校验分析,保证运行时不会危害虚拟机安全
通过字节码验证,也不能保证其一定是安全的,因为用程序去校验程序逻辑是无法做到绝对准确的
JDK6后新增StackMapTable属性描述方法体所有的基本块(Basic Block,指按照控制流拆分的代码块)开始时本地变量表和操作栈状态
在字节码验证时,JVM不再需要推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可,将类型推导转变为类型检查,从而节省校验时间
对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验
验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源
目的是确保解析行为能正常执行,若验证失败,会抛出IncompatibleClassChangeError的子类异常,如NoSuchMethodError
可用-Xverify:none关闭大部分的类验证以缩短JVM类加载的时间
为类中static变量分配内存并设置初始值
若为static变量,初始值为各类型零值,如下
若是static final变量,则存在ConstantValue属性,在准备阶段变量值会被初始化所指定的初始值
将常量池内的符号引用替换为直接引用
《Java虚拟机规范》未规定解析阶段发生的具体时间,只要求了在执行
这17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析
解析阶段会对方法或字段可访问性进行检查
JVM可对第一次解析的结果进行缓存,从而避免重复解析,但对于invokedynamic指令,必须等到程序实际运行时,解析动作才能进行
设当前代码所处的类为D,要把符号引用N解析为一个类或接口C的直接引用
上面解析完后还要验证D是否具备对C的访问权限,JDK9后还要检查模块权限,需具备
解析字段需先解析其所属类或接口(设为C)的符号引用,搜索name_index和descriptor_index都相匹配的字段,并返回其直接引用
上面解析能确保获取唯一解析结果,但实际上编译器会更加严格,如不允许继承结构中出现同名字段
解析完后还要权限验证,无访问权限抛出IllegalAccessError
解析类方法需先解析其所属类或接口(设为C)的符号引用,查找name_index和descriptor_index都匹配的方法
解析完后还要权限验证,无访问权限时抛出IllegalAccessError
解析接口方法需先解析其所属类或接口(设为C)的符号引用,查找name_index和descriptor_index都匹配的方法
JDK9后接口方法新增静态私有方法和模块化,无访问权限时抛出IllegalAccessError
初始化阶段就是执行类构造器
static{}块只能访问在其之前定义的变量,定义在它之后的变量可在static{}中赋值,但是不能访问
class Parent{
public static int A = 1;
static {
A = 2;
}
}
class Sub extends Parent{
public static int B = A;
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
多线程同时初始化类时,只有一个线程去执行类的
public class Test {
public static void main(String[] args) {
Runnable script= new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread() + " start");
LoopClass loopClass =new LoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
class LoopClass{
static {
if(true) {
System.out.println(Thread.currentThread() + " static{}");
while(true) {
}
}
}
}
如下为上面程序打印,说明线程2被线程1阻塞
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main] static{}