概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机类加载机制。在Java语言中,类的加载、连接和初始化过程都是在程序运行期间完成的,这是java作为动态语言的基础。另外值得注意的是上面提到的Class文件,并不一定值得是磁盘上的.class文件,而只需要是任何符合字节码规范的一串二进制字节流就可以了。
类加载时机
类的加载过程分为加载、验证、准备、解析、初始化、使用、卸载。其中加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段却不一定,为了支持运行时绑定它可能在初始化后开始。这些阶段通常都是互相交叉混合进行,通常都在一个阶段的执行过程中调用、激活另外一个阶段。
初始化阶段,有5种情况必须立即对类进行初始化(类的主动引用):
(1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化,此处需要注意已在编译期把结果放入常量池的静态字段除外。比如使用new关键词实例化类的对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法的时候。
(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4)当虚拟机启动时,用户需要指定一个要执行的主类。虚拟机会先初始化这个主类。
(5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先对其进行初始化。
类的被动引用(引用类时不会引起初始化):
(1)通过子类引用父类的静态字段
public class SuperClass{
static {
System.out.println("SuperClass init!")
}
public static int value = 123;
}
public class SubClass extends SuperClass{
static {
System.out.println("SubClass init!")
}
//输出SuperClass init!
public class NotInitialization{
public static void main(String[] args){
System.out.println(SubClass.value)
}
}
(2)通过数组定义来引用类
public class NotInitialization{
//没有输出
public static void main(String[] args){
SuperClass[] sca = new SuperClass[10];
}
}
(3)引用类中的常量
public class ConstClass{
static {
System.out.println("ConstClass init!")
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization{
//没有输出
public static void main(String[] args){
System.out.println(SubClass.value)
}
}
类的加载过程
1、加载
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问接口。
类的二进制流可以从jar、zip、war等压缩包中读取,也可以同网络中获取,甚至可以在运行是计算生成(例如java的动态代理就是利用ProxyGenerator.generateProxyClass()来为特定接口生成代理类的二进制字节流)。
2、验证
(1)文件格式验证。主要为验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。 通过验证后,字节流才会正确解析并存储于方法区中。
(2)元数据验证。主要为对字节码描述的信息进行语义分析,以保证其描述的信息符合java 语言的规范。 比如类是否有父类,是否是抽象类等。
(3)字节码验证。主要为对数据流和控制流进行分析。
(4)符号引用验证。这个校验发生在 虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段解析阶段发生。
3、准备
准备阶段是正式为类实例变量(static变量)分配内存并且设置类变量默认值的阶段,这些变量所使用的内存都将在方法区中进行分配。
4、解析
解析阶段简单的来说就是虚拟机将常量池内的符号引用替换为直接引用的过程。在执行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestaic、invokevirtual、idc、idc_w、multianewarray、new、putfield和putstatic这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、和调用点限定符7类符号引用进行。
5、初始化
类的初始化是类加载过程的最后一步,初始化阶段实质性类构造器<clinit>()
方法。
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态初始化块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。- 虚拟机会保证在子类的
<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕。 - 如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成
<clinit>()
方法。 - 接口也有
<clinit>()
方法,但是接口执行<clinit>()
方法不需要先执行父接口的<clinit>()
方法。只有当父接口中定义的变量被使用到时,才会执行<clinit>()
方法。 - 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其它线程都需要阻塞等待。