Java中类的生命周期
Java虚拟机通过加载、连接和初始化三个过程来使得一个Java类型可以被Java程序所使用,使用完后可以卸载掉该类。因此一个Java类的生命周期中包含如下几个阶段:
加载
类的加载是指将类的class文件中的二进制数据读入到内存中,并将其放在运行时数据区的方法区内,然后在内存中创建一个与之对应的java.lang.Class对象(规范并未说明Class对象位于哪里,Hotspot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构,这也是为什么我们能够在反射中通过类的Class对象可以访问到类的所有数据和方法,因为该Class对象就相当于描述了该类的结构。加载class文件的来源有以下几种:
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip、jar等归档文件中加载class文件
- 从专有数据库中提取class文件
- 将java源文件动态编译为class文件(例如动态代理就属于这种情况,类是在运行期生成)
链接
类被加载后,就进入到链接阶段,链接就是将已经读入到内存的类的二进制数据合并到虚拟机运行时环境中去。类的链接分为验证、准备和解析三个子阶段:
验证:确保被加载类的正确性,即检查class字节码是否符合JVM规范。类的验证主要完成了以下工作:
类文件结构的检查 语义检查 字节码验证 二进制兼容性验证
准备:为类的静态变量分配内存,并将其初始化为默认值,例如为引用类型赋予null值,为整形变量赋值为0
- 解析:在类的常量池中寻找类、接口、字段、方法的符号引用,将这些符号引用替换成直接引用
初始化
类的初始化就是按顺序为类的静态变量赋予正确的初始值和执行静态代码块。在Java程序中,静态变量的初始化有两条途径:1)在静态变量声明处进行初始化;2)在静态代码块中进行初始化。静态变量的声明语句,以及静态代码块都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序依次来执行它们。类的初始化步骤:
- 假如这个类还没有被加载和链接那就先进行加载和链接
- 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
- 假如类中存在初始化语句,那就依次执行这些初始化语句
另外,当Java虚拟机初始化一个类时,要求他所有的父类都已经被初始化,但是这条规则并不适应于接口:
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化
使用类
就是正常使用Java类,例如创建类的对象
卸载类
例如类在内存中对应的那个class对象被垃圾收集器回收了,那么这个类就被卸载了
关于类的使用方式
Java中对类的使用方式分为主动使用和被动使用,对类的主动使用会执行类的初始化过程,而被动使用则不会导致类的初始化。以下几种情况下属于对类的主动使用:
- 创建该类的实例((无论直接通过new创建出来的,还是通过反射、克隆、序列化创建的),注意创建该类型的数组对象不是对该类的主动使用,因为数组对象的类型是由JVM在运行期间动态生成的
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 调用JavaAPI中的某些反射方法,例如Class.forName(“com.test.Test”)是对Test类的主动使用
- 初始化某个类的子类(要求其祖先类都要被初始化,否则无法正确访问其继承的成员)
- Java虚拟机启动时被标明为启动类的类,即包含main方法的那个类(Java Test)
除了以上几种情况外,其他使用java类的方式都被看做类的被动使用,都不会导致类的初始化,即不会为类的静态变量赋予正确的初始值。所有java虚拟机实现必须在每个类或接口被java程序“首次主动使用“时才初始化他们。
主动使用会导致类的初始化,其超类均将在该类的初始化之前被初始化,但通过子类访问父类的静态字段或方法时,对于子类(或子接口、接口的实现类)来说,这种访问就是对子类的被动使用,或者说对于静态字段或静态方法来说,只有直接定义了该字段或方法的类才会被初始化。如下实例所示:
class Grandpa {
static String grandpaStr = "Grandpa";
static {
System.out.println("Grandpa was initialized...");
}
}
class Parent extends Grandpa{
static String parentStr = "Parent";
static {
System.out.println("Parent was initialized...");
}
static void doSomething(){
System.out.println("Parent doSomething was invoked...");
}
}
class Child extends Parent{
static String childStr = "Child";
static {
System.out.println("Child was initialized...");
}
}
public class MyTest2{
public static void main(String[] args) {
/**
* 使用Child中声明的静态变量是对Child的主动使用,
* 从而会对Child进行初始化,初始化Child类之前先要
* 初始化其父类及祖先类
*/
//System.out.println(Child.childStr);
//System.out.println("-----------------------");
/**
* 虽然是通过Child类访问到Parent类中的静态变量和调用父类中的
* 静态方法,但该静态变量和静态方法均不在不在Child类中定义,因
* 此不是对Child类的主动使用,而是对Parent类的主动使用
*/
System.out.println(Child.parentStr);
Child.doSomething();
}
}
输出结果如下所示,可以看出,虽然使用了Child
来访问Parent
中的静态成员parentStr
,但并不是对Child
类的主动使用,而是对Parent
类的主动使用。
Grandpa was initialized...
Parent was initialized...
Parent
Parent doSomething was invoked...
note:如果类中的static
字段同时还有final
关键字修饰,那么使用类名访问该字段时不一定会主动使用该类,这要看该字段是否能够在编译期就能够计算出确定的值。如下示例所示:
class ConstTest1{
public static final String STR = "ConstTest1";
static {
System.out.println("ConstTest1 was initialized...");
}
}
class ConstTest2{
public static final String STR = UUID.randomUUID().toString();
static {
System.out.println("ConstTest2 was initialied...");
}
}
public class MyTest3 {
public static void main(String[] args) {
/**
* 在编译期间就可以确定ConstTest1.STR的值,因此ConstTest1.STR
* 的值会直接存入到MyTest3类的常量池当中,之后MyTest3和ConstTest1
* 就没有任何关系了,甚至我们可以将ConstTest1的class文件删除,程序
* 依然能够正确运行。本质上MyTest3类并没有直接引用到定义常量的
* 类(ConstTest1),因此不是对ConstTest1的主动使用,不会触
* 发ConstTest1类的初始化
*/
System.out.println(ConstTest1.STR);
/**
* 在编译期间无法确定ConstTest2.STR的值,ConstTest2.STR的值不会放置到
* MyTest3的常量池当中,当程序运行时,会导致主动使用常量ConstTest2.STR
* 所在的类,从而初始化该类。
*/
System.out.println(ConstTest2.STR);
}
}
使用javap -c com.ctrip.flight.test.jvm.classloader.MyTest3
命令将生成的class文件反编译出来结果如下所示:
Compiled from "MyTest3.java"
public class com.ctrip.flight.test.jvm.classloader.MyTest3 {
public com.ctrip.flight.test.jvm.classloader.MyTest3();
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; 获取out对象
3: ldc #4 // String ConstTest1 将"ConstTest1"字符串从常量池中推向栈顶
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: getstatic #6 // Field com/ctrip/flight/test/jvm/classloader/ConstTest2.STR:Ljava/lang/String;
14: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: return
}
note:JVM指令ldc表示将int、float或String类型的常量值从常量池中推送至栈顶
从反编译的代码中也可以看出代码System.out.println(ConstTest1.STR);
中ConstTest1.STR
所对应的JVM指令ldc #4
已经跟ConstTest1
没有任何关系了,而代码System.out.println(ConstTest2.STR);
中ConstTest2.STR
所对应的JVM指令getstatic #6
依然跟ConstTest2
类有关系。
以下是对static final
修饰的常量成员对类主动使用的影响的一个总结:
- 对于能够在编译期间计算出具体值的常量:该常量会在编译期间存入到访问这个常量的方法所在类的常量池当中,调用类并没有直接饮用定义常量的类,因此不会触发定义常量的类的初始化
- 对于无法在编译期间计算出具体值得常量:该常量值不会被放置到调用类的常量池当中,这时程序运行时,会导致主动使用定义这个常量的类,从而导致该类被初始化
note:调用ClassLoader
类的loadClass
方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
接口的初始化
当一个类在初始化的时候,要求其父类都已经被初始化完成。然而这并不适应与接口,当一个接口被初始化的时候并不要求其父接口都完成了初始化,只有真正使用到父接口的时候(如引用父接口中定义的常量),才会初始化父接口。如下示例可以验证该结论:
interface MyInterfaceParent5{
/**
* 接口中定义的变量都是 public static final的,因此如果MyInterfaceParent5初始化
* 的话,非静态代码块中的语句"System.out.println("MyInterfaceParent5 was initialized...");"
* 一定会得到执行。
*/
Thread parentThread = new Thread(){
{
System.out.println("MyInterfaceParent5 was initialized...");
}
};
}
interface MyChildInterface5 extends MyInterfaceParent5{
Thread childThread = new Thread(){
{
System.out.println("MyChildInterface5 was initialized...");
}
};
}
class MyChildClass5 implements MyInterfaceParent5{
public static int a = 5;
static {
System.out.println("MyChildClass5 was initialized...");
}
}
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChildClass5.a);
System.out.println(MyChildInterface5.childThread.getName());
}
}
我们同时使用-XX:+TraceClassLoading
选项跟踪类的加载情况,输出结果如下所示:
[Loaded com.ctrip.flight.test.jvm.classloader.MyTest5 from file:/Users/peter/MyWork/study/IdeaWorkSpace/jvm/out/production/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.ctrip.flight.test.jvm.classloader.MyInterfaceParent5 from file:/Users/peter/MyWork/study/IdeaWorkSpace/jvm/out/production/classes/]
[Loaded com.ctrip.flight.test.jvm.classloader.MyChildClass5 from file:/Users/peter/MyWork/study/IdeaWorkSpace/jvm/out/production/classes/]
[Loaded java.net.Inet6Address$Inet6AddressHolder from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.ctrip.flight.test.jvm.classloader.MyInterfaceParent5$1 from file:/Users/peter/MyWork/study/IdeaWorkSpace/jvm/out/production/classes/]
MyChildClass5 was initialized...
5
[Loaded java.io.IOException from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.net.SocketException from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.net.SocksSocketImpl$3 from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.net.ProxySelector from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.ctrip.flight.test.jvm.classloader.MyChildInterface5 from file:/Users/peter/MyWork/study/IdeaWorkSpace/jvm/out/production/classes/]
[Loaded sun.net.spi.DefaultProxySelector from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.net.spi.DefaultProxySelector$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.net.NetProperties from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.net.NetProperties$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.ctrip.flight.test.jvm.classloader.MyChildInterface5$1 from file:/Users/peter/MyWork/study/IdeaWorkSpace/jvm/out/production/classes/]
MyChildInterface5 was initialized...
Thread-0
并没有输出”MyInterfaceParent5 was initialized…”,也就是MyInterfaceParent5
并没有初始化,但MyInterfaceParent5
是已经被加载进来了。MyInterfaceParent5
的实现类MyChildClass5
和子接口MyChildInterface5
都被加载进来并被初始化。