JVM学习4--类装载、生命周期
一、类的生命周期
首先要明确,类的生命周期是有五个阶段,而我们平时所说的类加载或者类装载,是指前三个阶段,即:加载、连接、初始化。
二、类装载概述
在这块,大体上讲一下类装载的概念,然后有几个demo,产生的现象我们在细说加载、连接、初始化三个阶段时解释。
类装载的条件: Class只有在被使用的时候才会被装载,Java虚拟机不会无条件地装载Class。Java虚拟机规定,一个类或接口被使用之前一定要进行初始化。这里的“使用”,指的是直接使用,也称直接引用,直接引用有以下几种情况:
- 当创建一个类的实例时,比如new,或者用过反射、克隆、类序列化。
- 当调用类的静态方法时,即当使用了字节码invokestatic指令。
- 当使用类或接口的静态字段时(final常量除外),比如,使用getstatic或者putstatic指令。
- 当使用java.lang.reflect包中的方法反射类的方法时。
- 当初始化子类时,要求先初始化父类。
- 作为启动虚拟机,含有main()方法的那个类。
例① 是一个直接引用和间接引用的例子:
class Parent{ static { System.out.println("Parent init"); } public static int v = 100; } class Child extends Parent{ static { System.out.println("Child init"); } } public class IndirectReferencing { public static void main(String[] args) { // 直接引用(父类会先初始化,子类也会初始化) // 如果两步不分开执行,父类也只会初始化一次,这是ClassLoader做的保障,下面会提到 Child child = new Child(); //间接引用(子类不会初始化) System.out.println(Child.v); } }
例② 引用常量也不会引起初始化:
class FinalField{ public static final String cons = "CONST"; static { System.out.println("FinalField init"); } } public class UseConstant { public static void main(String[] args) { // FinalField类不会初始化 System.out.println(FinalField.cons); } }
我们可以在运行程序时加上-XX:+TraceClassLoading参数观察类的加载情况:
例① 间接引用的情况:可以看到子类没有被初始化,但是子类其实也被加载进来了,这就是类生命周期第三阶段初始化的特点:直接引用初始化,间接引用不初始化。
[Loaded JvmTest.ClassLoaderT.Parent from file:/D:/IntelliJ-idea/experiment/out/production/experiment/]
[Loaded JvmTest.ClassLoaderT.Child from file:/D:/IntelliJ-idea/experiment/out/production/experiment/]
Parent init
100
[Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar]
例② 可以发现这次FinalField都没有加载,那么我们可以看出一个事情,类就算不加载,常量依然可以使用,这是为什么呢?下面说连接阶段细说。
[Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar]
CONST
[Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar]
也可以用字节码观察一下程序,javap -c xxx.class javap -verbose xxx.class :
例②
public class JvmTest.ClassLoaderT.UseConstant { public JvmTest.ClassLoaderT.UseConstant(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String CONST 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }
观察main函数,使用了ldc,在字节码偏移3的位置,将常量池第4项入栈,在这里第四项是:
#4 = String #25 // CONST
可以看到,在编译后的UseConstant类中,并没有引用FinalField类,而是直接把常量存到了常量池中,所以FinalField自然不会加载。
三、类装载之加载阶段
加载类处于类装载的第一阶段。在加载类时,JVM完成了以下工作:
- 通过类的全名,获得类的二进制数据流。
- 解析类的二进制流为方法区内的数据结构。
- 创建java.lang.Class类的实例,表示该类型。
在获得到类的二进制信息后,java虚拟机就会处理这些数据,最终转换为一个java.lang.Class的实例,java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据。通过Class类提供的接口,可以访问一个类型的方法、字段等信息。例子:Class.forname("java.lang.String")
四、 类装载之连接阶段
1. 验证:目的是保证加载的字节码是合法、合理并符合规范的。
2. 准备:虚拟机为类分配相应的内存空间,并设置初始值。
final常量,直接就被赋值了。
static普通静态变量,准备阶段只赋默认的初始值,在clinit中才会被设置为制定个值。例如:
public static int v = 1; 准备阶段v被设置为0,clinit(初始化阶段)中设置为1。
回答上面例2的疑问,为什么FinalField没有被加载呢?
因为在连接阶段,常量可以直接被赋值,因此,FinalField类根本没被引用(直接引用或间接引用),自然不会被加载。
3. 解析类: 将类、接口、字段、方法的符号引用转为直接引用
在程序实际运行时,只有符号引用是不够的,例如当println()方法被调用时,系统需要明确知道该方法中的位置。以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个方法时,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法、或者字段,但只存在符号引用,不能确定系统中一定存在该对象。
四、类装载之初始化阶段
初始化是类装载的最后一个阶段。此时,类才会开始执行Java字节码。初始化阶段的重要工作是执行类的初始化方法<clinit>。方法<clinit>是由编译器自动生成的,它是由静态成员变量的赋值语句以及static语句块合并产生的。