JVM(三)类和对象的生命周期
本文从虚拟机加载和执行层面探讨下类或者接口(下文统一使用类)、对象的生命周期。
命令行编译和执行代码
在初学Java的时候很多人都学过在没有IDE的情况下编译(javac)和执行(java)代码,后来你会彻底爱上IDE,它可以自动编译和管理依赖,你只需要运行代码就行了,我们这里先来回顾下如何通过命令行编译和执行代码,项目目录结构如下:
+-| com
|----+- deepoove
|-----------+- java8
|---------------+- def
|-------------------\- Apple.java
|---------------\- ClassLoaderTest.java
我们写个非常简单的类ClassLoaderTest,引用了Gson类和目录下面的Apple类:
package com.deepoove.java8;
import com.deepoove.java8.def.Apple;
import com.google.gson.Gson;
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
System.out.println("Apple.class: " + Apple.class.getClassLoader());
Apple[] array = new Apple[5];
System.out.println(array.getClass() + ": " + array.getClass().getClassLoader());
ClassLoader classLoader = Gson.class.getClassLoader();
System.out.println("Gson.class classloader:");
while (null != classLoader) {
System.out.println("\t" + classLoader);
classLoader = classLoader.getParent();
}
System.out.println("String.class: " + String.class.getClassLoader());
}
}
接下来就可以进行编译了,我们进入com同级目录下执行javac编译命令:
javac -sourcepath . -classpath /Users/Sayi/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.5/de8829/gson-2.8.5.jar com/deepoove/java8/ClassLoaderTest.java
-sourcepath指定源代码路径,-classpath指定了依赖类的路径,命令执行成功后,会在ClassLoaderTest.java同级目录下,生成字节码文件ClassLoaderTest.class。
通过java命令可以执行字节码文件了:
$ java -classpath .:/Users/Sayi/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.5/de8829/gson-2.8.5.jar com/deepoove/java8/ClassLoaderTest
Apple.class: sun.misc.Launcher$AppClassLoader@2a139a55
class [Lcom.deepoove.java8.def.Apple;: sun.misc.Launcher$AppClassLoader@2a139a55
Gson.class classloader:
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@4aa298b7
String.class: null
注意的是,在执行字节码时,classpath路径多了一个点和冒号: .:,这就和类加载机制有关系了,会尝试从当前目录和gson.jar寻找并加载类,java命令会把用户类路径存储在java.class.path属性,可以通过查看这个属性看到应用从哪些路径加载类,这个属性的配置可能来自于:
- 默认值是 '.', 表示当前目录或者子目录下寻找所有的字节码类文件
- 通过系统环境变量CLASSPATH改写默认值
- 通过
-classpath或者-cp改写默认值和CLASSPATH值
因为路径会被改写,所以在执行代码时 . 和 gson.jar 都需要在-classpath中指定。
字节码文件和javap
代码执行过程是首先从字节码加载类,然后从一个static main方法为入口执行。字节码文件是一个类的二进制表示,我们需要理解它的结构才能很好的阅读,同时也有很多框架提供了字节码操纵:cglib、javassist等,我们可以通过javap命令反编译字节码文件查看:
Classfile /Users/Sayi/reflections-example/bin/com/deepoove/example/Reader.class
Last modified 2018-11-19; size 435 bytes
MD5 checksum eb2714fb2d57cda321faf74167890254
Compiled from "Reader.java"
public interface com.deepoove.example.Reader
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #2 // com/deepoove/example/Reader
#2 = Utf8 com/deepoove/example/Reader
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 read
#6 = Utf8 (Ljava/io/File;)Ljava/util/List;
#7 = Utf8 Signature
#8 = Utf8 (Ljava/io/File;)Ljava/util/List<Ljava/lang/String;>;
#9 = Utf8 url
#10 = Utf8 MethodParameters
#11 = Utf8 getCode
#12 = Utf8 ()I
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/deepoove/example/Reader;
#18 = Utf8 SourceFile
#19 = Utf8 Reader.java
{
public abstract java.util.List<java.lang.String> read(java.io.File);
descriptor: (Ljava/io/File;)Ljava/util/List;
flags: ACC_PUBLIC, ACC_ABSTRACT
Signature: #8 // (Ljava/io/File;)Ljava/util/List<Ljava/lang/String;>;
MethodParameters:
Name Flags
url
public int getCode();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: bipush 100
2: ireturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lcom/deepoove/example/Reader;
}
SourceFile: "Reader.java"
字节码二进制文件中并不是所有字段都是存在的,比如MethodParameters(需要在编译时加上-parameter参数)、LocalVariableTable(需要在编译时加上-g或者-g:vars)等, 如果有些功能依赖于字节码中的这些字段,那么就要确保在编译的时候加上合适的参数。
类的生命周期
类从字节码文件被加载开始到使用,直至最后被卸载是它完整的生命周期。

类在被Link前,一定会完全被Load;类在被Initialization前一定会完全被verification和Preparation,Resolution是一个可选的阶段。
下面对这些步骤作一个简单介绍。
Load 加载
Load涉及到通过一个binary name和类加载器将一个二进制数据(classFile)分配到方法区,构造Class对象的过程,这个过程是由ClassLoader和其子类实现的。
binary name是一个类的二进制名称,比如java.security.KeyStore$Builder$FileBuilder$1。
在Load过程中,可能发生的异常有:
- ClassCircularityError:A class or interface could not be loaded because it would be its own superclass or superinterface
- ClassFormatError:二进制数据格式错误
- NoClassDefFoundError、ClassNotFoundException:找不到关联的Class的定义
- OutOfMemoryError:因为涉及到方法区的分配,所以可能会导致内存溢出
Link 链接
链接主要分为三步:verification、Preparation和Resolution,在链接的过程,涉及到内存分配,有可能会抛出OutOfMemoryError。
verification
verification是对字节码文件是否满足一些限制的验证,这一步可能会导致其它的类被Load,但是其他类不是必须要被verification。
当校验无法通过时,会抛出VerifyError异常。
Preparation
对类的静态字段进行创建和初始化默认值,注意的是,明确的对类的一个静态字段赋值是Initialization的一部分,不属于Preparation。
Resolution(Optional)
Resolution是决定类对它引用的其它类或者接口符号引用的过程。可能会抛出如下异常:
- IllegalAccessError:没有权限引用
- InstantiationError:抽象类无法实例化
- NoSuchFieldError:找不到属性
- NoSuchMethodError:找不到方法
- IncompatibleClassChangeError:不兼容的类改变
Initialization 初始化
当调用了静态方法、字段的或者明确创建对象、利用反射API等场景都会触发类的初始化,执行类的静态初始化方法。
Unloading 卸载
一个类可能会被卸载,当且仅当它的加载器被回收,注意的是,bootstrap class loader加载的类不会被卸载。
对象的生命周期
对象从被实例化到使用,直至最后被垃圾回收是它完整的生命周期。

instantiation and initialization 实例化和初始化
一个对象可能会被显式或者隐式创建,比如new操作符,比如lambda表达式,字符串常量和装箱等。
对象创建都会涉及到内存分配。
Garbage collection and Finalization of Objects 回收
对象在不可达时会被垃圾回收标记,在第一次标记时会调用每个对象的protected void finalize() throws Throwable { }方法,这是被垃圾回收器自动调用的,在进入第二次标记时,这个对象将会被回收。
注意的是,finalize方法有且仅会被调用一次,我们也可以重写finalize方法,将当前对象重新挂到引用链时从而避免被垃圾回收,但是第二次不可达时则会直接被回收,不会再执行finalize方法。
Class.forName vs ClassLoader
我们已经知道了类从加载到初始化直至被卸载的流程,类加载过程是由ClassLoader实现,JDK还提供了Class.forName加载类获得Class对象,它们有什么区别呢?
数组加载
数组不是由类加载器加载的,它们是在需要的时候被虚拟机自动创建。但是每个数组类里面都有一个ClassLoader的引用,这个值和数组的元素类型的ClassLoader是相同的,如果是原生类型,则这个CLassLoader为空。
我们可以换个角度思考,由于CLassLoader是需要binary name,而数组其实是没有一个二进制名称的,所以无法通过ClassLoader加载,但是数组有一个代表数组类的名称,所以可以通过CLass.forName去加载:
Class<?> forName = Class.forName("[Lcom.deepoove.java8.def.Apple;");
上面这串代码就加在了Apple[]数组类型,由于没有指定维度,所以是无法创建实例的,数组初始化可以使用反射包下的java.lang.reflect.Array.newArray(Class<?>, int)来创建数组实例。
JDBC驱动类加载
学过JDBC的人都知道第一行代码就是加载JDBC驱动:
Class.forName("com.mysql.jdbc.Driver");
从含义上来说,它真的是加载驱动吗?如果只是加载这个类,只要这个类在用户路径下何须手动加载,即使要加载,ClassLoader就可以完成加载?
这里就涉及到类的生命周期和ClassLoader内在实现问题了,ClassLoader加载的类并不会进行Initialization初始化阶段,而Class.forName("")会进行类的初始化,调用Driver类相关的静态初始化方法,而这些初始化方法的代码才是加载JDBC驱动真正要做的事情,我们来看看Class.forName的源码:
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller) throws ClassNotFoundException;
看到native方法支持initialize和ClassLoader参数,initialize为true则会进行类的初始化。
扩展:JDBC 4.0 以后已经不需要通过Class.forName加载并初始化驱动类了,通过SPI机制实现了加载和初始化。
参考资料
- The Lifetime of a Type
- Java Language Specification-Chapter 12. Execution
- Chapter 5. Loading, Linking, and Initializing
总结
我们知道应用程序如何加载类以及初始化的时机,我们学到了字节码文件从加载到执行的整个过程,理解类和对象的生命周期,可以帮助我们更好的理解问题和优化我们的程序。
下一篇文章,我们研究下类加载机制。