JVM类加载机制
知识点:
类从被加载到虚拟机内存中使用,到卸载出内存,它整个生命周期包括:加载(Loading)、链接(Linking)、初始化(Initialize)、使用(Using)和卸载(Unloading)阶段,其中链接阶段可细分为验证(Verification)、准备(Preparation)、解析(Resolution);
整个阶段发生顺序流程如下图所示:
加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类加载过程按这个大概流程加载,但是解析阶段不一定固定的,它在某些情况下初始化阶段后再开始执行,原因是为了支持Java语言的运行时绑定(动态绑定)。
主要考虑加载、验证、准备、解析、初始化五个步骤,这构成一个完整的类加载过程。
加载
有两个时刻会触发类加载:
-
预加载,虚拟机启动时加载,加载是JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包是程序运行时常用的,比如:java.io、java.lang、java.util等等,这些包随着虚拟机一起加载。查看类加载信息,可写一个空的main函数,设置虚拟机参数为[-XX:+TraceClassLoading]来获取。
-
运行时加载。虚拟机使用一个.class文件时,先去内存中查看这个.class文件是否被加载,如果没有,则会按照类的全限定名来加载这个类。
加载阶段主要做了如下三个步骤:
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将类信息、静态变量、常量这些.class文件所代表的静态存储结构转化为方法区的运行时数据结构
- 在java堆中生成一个这个类的Java.lang.Class对象,作为对方法区中这些数据访问入口
验证
链接阶段第一步骤-为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。
验证阶段做了如下步骤:
- 文件格式验证
有关于jdk版本文件格式问题,字节码文件第5到8个字节表示是该字节码文件主要版本号,验证会对这4个字节做验证,高版本的JDK向下兼容低版本的.class文件,但不能运行超过该版本之后的class文件,如果运行,会抛出java.lang.UnsupportedClassVersionError。 - 元数据验证
- 字节码验证
- 符号引用验证
准备
准备阶段是为类静态变量分配内存并设置其初始值的过程,这些变量所使用的的内存都将在方法区中分配。
注意点:
- 这进行内存分配仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中
- 这阶段赋初始值的变量指的是那些不被final修饰的static变量,比如: [public static int value = 11],value在准备阶段过后赋值为0而不是11,value赋值为11的操作将在初始化阶段才进行,比如[public static final int value = 11 ]的情况跟上述不一样,在准备阶段,虚拟机会给value赋值为11;
解析
解析阶段是虚拟机将常量池内的符号引号替换为直接引号的过程。
首先看符号引号和直接引号的区别:
符号引号
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
符号引号是对于类、变量、方法的描述。符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中。
直接引用
直接引用是直接指向目标的指针,相对偏移量;直接引号是和虚拟机实现的内存布局相关,同一个符号引号在不同的虚拟机上编译出来的直接引用一般不会相同,如果有了直接引用,那引用的对象必定已经存在java堆内存中。
初始化
初始化阶段是类加载过程的最后一个步骤,初始化就是给静态变量赋予用户指定的值以及执行静态代码块。
注意点:
虚拟机会保证类的初始化在多线程场景中被正确地加锁、同步。
如果多个线程同时去初始化同一个类,那么只有一个线程去执行这个类的clinit()方法[类加载方法],其他线程都要阻塞等待,直到获取锁的线程执行clinit()方法完成,如果在一个类的clinit()方法中有耗时很长操作,就可能造成多个进程组撒,不过其他线程虽然会阻塞,但是执行clinit()方法的那个线程执行完后,其他线程不会再次进入clinit()方法,因为同一类加载器下,一个类只会初始化一次。
Java虚拟机规定有且只有以下场景必须立即对类进行初始化,这些场景称对一个类进行主动引用。
- 使用new关键字实例化对象、读取或者设置一个类的静态字段、调用一个类的静态方法
- 使用java.lang.reflect包中的方法对类进行反射调用
- 初始化一个类,发现其父类还没初始化
- 虚拟机启动,虚拟机先初始化用户指定的包含main()方法的那个类