Earth Guardian

You are not LATE!You are not EARLY!

0%

Java 类加载机制

类加载机制:虚拟机把描述类的数据从 Class 文件加载到内存,并对数据校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

类加载过程

类从被加载到虚拟机内存中开始,到类卸载出内存为止,它的整个生命周期包括:加载 Loading, 验证 Verification,准备 Preparation,解析 Resolution,初始化 Initialization,使用 Using 和卸载 Unloading 共七个阶段。其中验证、准备、解析这三个阶段部分统称为连接 Linking,七个阶段出现的顺序如图所示:

0078-java-class-loading-seq.png

其中:加载、验证、准备、初始化、卸载这五个阶段的顺序是固定的,类的加载过程必须按照这个顺序执行,而解析有可能会在初始化之后才开始(比如:动态绑定,也称为晚绑定、动态分派 Difference between Binding and Dispatching in Java, wiki:Late_binding in java)。
类加载全过程也就是:加载、验证、准备、解析、初始化这五个阶段。

加载 Loading

加载是类加载过程的一个阶段,加载需要完成三件事:

  • 通过类的全限定名获取定义此类的二进制字节流。这个字节流可以是从本地 Class 文件、网络下载、使用动态代理运行时生成等方式获取
  • 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

非数组类的加载阶段,也就是通过全限定名获取类的二进制字节流,这个过程可控性很强,可以默认使用系统提供的类加载器去加载,也可以自定义加载器实现。
而数组类本身不通过类加载器创建,它是由虚拟机直接创建的,但是数组类的元素类型 Element Type (也就是数组类型)还是要靠类加载器加载。

加载完成后,虚拟机将外部的二进制字节流,按照虚拟机所需格式存储到方法区中,并在方法区中实例化 java.lang.Class 对象,这个类对象将作为程序访问类型数据的接口。

验证 Verification

验证阶段确保加载的二进制字节流包含的信息符合虚拟机需求,以及做一些安全检查。

  • 文件格式验证
    比如验证文件是否以魔数开头 0xCAFEBABE 开头;主次版本号是否符合虚拟机范围;Class 文件本身是否有被删除信息等等。
  • 元数据格式验证
    对字节码描述的信息进行语义分析,确保符合 Java 语言规范。比如:类是否具有父类;是否能被继承;是否为抽象类;字段、方法是否和父类矛盾等等。
  • 字节码验证
    通过数据流和控制流分析,确定程序语义是否合法符合逻辑,使用类型检查完成数据流分析。比如:操作数栈的数据类型和指令能配合工作;确保类型转换是安全的等等。
  • 符号引用验证
    这个转换动作主要在解析阶段发生,确保解析阶段能够正常执行。符号引用验证主要对类自身以外的信息进行校验:符号引用中的全限定名能否找到对应的类;指定类中是否存在符合方法的字段描述符;符号引用中的访问控制符是否可以被当前类访问等等。

准备 Preparation

准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段,这些变量都在方法区分配内存。需要注意两点:

  • 内存分配仅仅包含类变量(static 变量),并不包含实例变量(分配到堆内存)
  • 初始值是指数据类型的零值,而不是声明变量时的赋值

0078-class-loading-default-value.png

1
2
public static int number = 123;
public static final int value = 100;

示例中 number 在准备阶段值为 0,而 value 因为是常量 static final ,在准备阶段会被直接赋值为 100 。

解析 Resolution

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。

  • 符号引用
    以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能准确定位到目标即可。符号引用字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
  • 直接引用
    可以只直接指向目标的指针、偏移量或者间接定位到目标的句柄。有了直接引用,那么引用目标一定是已经在内存中存在了。

解析动作主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。这几类符号引用的解析过程,《深入理解 Java 虚拟机》第七、八章中有详细介绍。

初始化 Initialization

类初始化阶段是类加载过程的最后一步,该阶段才真正开始执行 Java 程序代码。在准备阶段,类变量仅仅赋为零值,在类初始化阶段才会执行代码并赋值。类初始化阶段是执行类构造器 <clinit>() 方法的过程

  • <clinit>() 方法是由编译器自动收集类中所有类变量的赋值动作和 static{} 静态语句代码块合并产生的,收集顺序就是代码中出现的顺序。静态语句块只能访问定义在之前的静态变量,定义在之后的变量只能赋值不能访问
  • <clinit>() 方法与类实例构造器(<init>())不同,它不需要显示调用父类构造器,虚拟机会确保子类 <clinit>() 方法之前执行完父类的 <clinit>() 方法,也就是说虚拟机第一个被执行的 <clinit>() 方法肯定是 java.lang.Object 的。这也意味着父类的静态语句会先于子类执行。
  • <clinit>() 方法对于类或者接口并不是必须的,类中可以没有静态变量赋值及静态语句块,编译器也就不会生成 <clinit>() 方法
  • 接口中不能有静态语句块, 但可以有静态变量定义和赋值,所以也会生成 <clinit>() 方法。但需要注意接口与类不同的是:执行接口 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法,只有父接口的变量在使用时才会执行;实现接口的类初始化时也不会执行接口的 <clinit>() 方法
  • 虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确的加锁、同步。多线程同时执行类初始化,那么只会有一个线程去执行类的 <clinit>() 方法,其他线程会阻塞等待直到 <clinit>() 方法执行完毕。
  • 同一个类加载器下,类只会被初始化一次,也就是 <clinit>() 方法只会被执行一次

<clinit>() 方法是类初始化过程,即类加载过程的初始化阶段;<init>() 是类实例化过程,即遇到 new 关键字生成类对象阶段。

  • <clinit>()
    类构造器,包含类变量(static 变量)初始化赋值,静态语句代码块(static{})。
  • <init>()
    实例构造器,包含实例变量初始化赋值,构造语句代码块({}),构造方法。

类加载的时机

类初始化时机

什么时候开始进行类加载过程中的第一个阶段:加载?虚拟机并没有明确规定。但是虚拟机严格规定了:有且只有 5 种情况必须对类进行初始化(也就是一定会触发类加载过程):

  • 遇到 new, getstatic, putstatic, invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这几条指令的场景如下
    使用 new 关键字实例化对象;读取或设置类的静态字段(被 final 进入常量池的静态字段除外);调用一个类的静态方法。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,需要先触发初始化
  • 初始化一个类的时候,如果发现父类还没有初始化,先触发父类初始化
  • 用户指定要执行额主类:即包含 main 主类的先初始化
  • 如果 java.lang.invoke.MethodHandle 实例最后解析的结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,如果没有初始化会触发其初始化

主动/被动引用

上面明确的 5 种类初始化场景,称为类的主动引用;而不会触发类初始化的引用,称为类的被动引用。主动/被动引用,是否触发类加载过程的加载阶段,虚拟机没有明确规定,当前测试的虚拟机只要引用了,都会触发类加载过程的加载阶段
被动引用有如下几种情形:

  • 子类引用父类静态字段
    子类不会被初始化,而父类会被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。
  • 类数组
    数组引用类,不会触发类初始化。
  • 静态常量
    静态常量如果在编译期能够被确定,则不会触发类初始化,甚至都不会进入类加载阶段,比如字符串常量。它们会直接放入常量池,并且在编译器优化中,直接将该常量放入到引用类的常量池中,也就是说生成的引用类 Class 文件中并不包含被引用类的符号引用,它们在 Class 文件中毫无关系了。
    而编译期无法确定,必须在运行时才能确定的字段,这类静态常量会导致类初始化(只有在类初始化阶段才开始执行代码),比如当前时间 static final String STATIC_TIME = System.currentTimeMillis() + "";
  • 反射 Classloader.loadClass
    在使用反射过程中,ClassLoad.loadClass 只会触发类加载阶段,不会执行类初始化。
  • 反射 Class.forName
    在使用反射过程中,Class.forName 时会同时触发类加载和类初始化阶段,默认使用调用类的类加载器进行类加载

类加载器

类加载阶段中:通过一个类的全限定名来获取描述此类的二进制字节流,这个过程是在虚拟机外部实现的,实现这个过程的模块即为类加载器
对于任一个类:都需要由类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性。每个类加载器,都有一个独立的类名称空间。也就是说,比较两个类是否相等,只有在同一个类加载器中加载才有意义。即使两个类属于同一个 Class 文件,由同一个虚拟机加载,只要加载它们的类加载器不同,这两个类必定不相等。相等的测试,可以是 Classequals(), isAssignableFrom(), isInstance() 等方法,或者 instanceof 关键字。

类加载器分类

  • 启动类加载器 Bootstrap ClassLoader
    这个是 C/C++ 实现,虚拟机的一部分,负责加载系统类,JAVA_HOME\lib 目录或者 -Xbootclasspath 参数指定路径下的类库(rt.jar 等基础库)加载到虚拟机内存中。启动类加载器无法被用户程序直接引用,引导类加载器没有对应的 ClassLoader 。虚拟机只加载 JAVA_HOME\lib 按照文件名识别的类库,自定义类库即使放到这个目录下,虚拟机并不会去加载。
  • 扩展类加载器 Extension ClassLoader
    sun.misc.Launcher$ExtClassLoader 实现,负责加载 JAVA_HOME\lib\ext 中的标准扩展类库,用户可以直接使用这些扩展类加载器。如果将自定义 jar 包放到这个路径下,扩展类加载器将会加载这些类。
  • 应用程序类加载器 Application ClassLoader
    sun.misc.Launcher$AppClassLoader 实现,负责加载 CLASSPATH 中的类库及应用类。这个类加载器是 ClassLoader.getSystemClassLoader() 的返回值,所以一般称为系统类加载器,用户可以直接使用。

通常在搭建 Java 开发环境时,都需要添加 JAVA_HOME, CLASSPATH 两个全局环境变量,就是为了给类加载器指定路径的。

双亲委派模型 Parents Delegation Model

0078-java-classloading-parents-delegation-model.jpg

上图所示的类加载器之间的层次关系,称为类加载器的双亲委派模型。该模型要求除了顶层启动类加载器外,其余的类加载器都应当有自己的父加载器。这里的父子关系不是继承关系,而是组合关系来复用父类代码。
双亲委派模型原则:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

破坏双亲委派模型

双亲委派模型并不是强制性的约束,而是 Java 推荐使用这种方式。

  • 自定义类加载器
    后面源码分析中可以看到 ClassLoader.loadClass() 方法实现了双亲委派模型,该方法可以被重写 loadClass,但是一般情况下会遵循双亲委派模型。所以 JDK 中定义了 findClass 方法,推荐自定义类加载器重写该方法,既不会破坏双亲委派模型,又可以实现自己的加载器。
  • 线程上下文加载器 Thread Context ClassLoader
    可以通过 Thread.setContextClassLoader 来设置。如果线程创建时没有设置,它将从父线程中继承一个;如果全局范围内都没有设置,默认使用应用程序类加载器。它打破了双亲委派模型,也就是父加载器请求子加载器去完成类加载的动作。
    系统默认的上下文加载器为系统加载器,可以参考 Launcher 源码。
  • 代码热替换 HotSwap 和模块热部署 HotDeployment
    应用程序像计算机的外设一样,可以热插拔鼠标、U 盘等,不需要重启。自定义加载器可以实现这些功能,并且有非常广泛的应用,如 Android 热部署、插件化等。

类加载源码

虚拟机加载程序的入口类:sun.misc.Launcher.java,学习类加载机制也选这个文件开始分析。

类图结构

0078-ClassLoader-uml.png

从类图结构中可以看出:AppClassLoaderExtClassLoader 都是 ClassLoader 的子类,他们在类关系中是并行的,并不是父子结构。

Launcher 源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// 1. Launcher
public class Launcher {
...
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
private ClassLoader loader;
...
public static Launcher getLauncher() {
return launcher;
}

public Launcher() {
Launcher.ExtClassLoader extcl;
try {
// 2. Launcher 构造方法中实例化 ExtClassLoader
extcl = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError
("Could not create extension class loader", var10);
}

try {
// 3. Launcher 构造方法中实例化 AppClassLoader
// 并将 ExtClassLoader 传递给 AppClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(extcl);
} catch (IOException var9) {
throw new InternalError
("Could not create application class loader", var9);
}

// 4. 设置 ContextClassLoader 类加载器
Thread.currentThread().setContextClassLoader(this.loader);
...
}

/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}

// 5. AppClassLoader
static class AppClassLoader extends URLClassLoader {
...

public static ClassLoader getAppClassLoader(final ClassLoader var0)
throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0]
: Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged
(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0]
: Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}

AppClassLoader(URL[] var1, ClassLoader var2) {
// 6. AppClassLoader 的父加载器设置为 ExtClassLoader
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
...
}

// 7. ExtClassLoader
static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader()
throws IOException {
final File[] var0 = getExtDirs();

try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged
(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
...
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}

void addExtURL(URL var1) {
super.addURL(var1);
}

public ExtClassLoader(File[] var1) throws IOException {
// 8. ExtClassLoader 的父加载器为 null
// 实际其父加载器默认为 BootstrapClassLoader
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess()
.getURLClassPath(this).initLookupCache(this);
}

private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
...
}
...
}

...
}

Launcher, ExtClassLoader, AppClassLoader 三个类的构造方法:

  • Launcher
    依次实例化 ExtClassLoader, AppClassLoader,并将 ExtClassLoader 的实例传递给 AppClassLoader;设置当前线程的加载器 setContextClassLoader,即每个线程默认加载器是 AppClassLoader
  • ExtClassLoader
    其父加载器设置为空 null,虚拟机默认 null 为启动类加载器,即其父加载器是 BootstrapClassLoader
  • AppClassLoader
    其父加载器设置为 ExtClassLoader

各个类加载器对应的属性

从上面 Launcher 的源码中也可以看出,各个加载器的路径都是通过属性来读取的:

  • 启动类加载器 Bootstrap ClassLoadersun.boot.class.path
  • 扩展类加载器 Extension ClassLoaderjava.ext.dirs
  • 应用程序类加载器 Application ClassLoaderjava.class.path

ClassLoader 源码分析

ClassLoader 包含几个重要的方法,其中 ClassLoader.loadClass 中实现了双亲委派模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public abstract class ClassLoader {
...
public Class<?> loadClass(String var1) throws ClassNotFoundException {
return this.loadClass(var1, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 1. 先确认类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 如果没有被加载,父加载器是否为空
// 不为空则父加载器加载,依次递归
c = parent.loadClass(name, false);
} else {
// 3. 父加载器为空,则启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 4. 如果父加载器抛出 ClassNotFoundException
// 说明父加载器无法完成类加载请求
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 5. 父加载器无法完成加载或者其他原因没有加载成功
// 则调用本身的 findClass 进行类加载
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime()
.addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime()
.addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
...
public final ClassLoader getParent() {
if (this.parent == null) {
return null;
} else {
...
return this.parent;
}
}
protected final Class<?> defineClass(...) throws ClassFormatError {
...
}

public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
...
return scl;
}

private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
...
}
sclSet = true;
}
}
}
  • loadClass
    loadClass 传入的参数是类全名,比如:com.***.Test, com.***.Test$innerclass,并返回一个 Class 类型的实例。它实现了双亲委派模型,如果父加载器都没有加载成功,则调用 findClass 来查找。
  • findClass
    findClass 方法中直接抛出异常,也就是必须由子类实现。我们自定义类加载器时,通常需要重写 findClass ,而不是 loadClass(会覆盖掉双亲委派)。该方法是 protected 的,也就是外部类并不需要主动调用,参数为 loadClass 传入的类全名。
  • getParent
    获取父加载器。
  • defineClass
    有多个重载方法,作用是将一个 byte 数组转换为 Class 类的实例,通常是将 .class 文件转换为二进制数组并通过 defineClass 来获取类实例。这个方法非常重要,将指定类全名的二进制数组转换成运行时内存数据结构,并校验有效性等等。
  • getSystemClassLoader
    静态方法,获取系统类加载器。系统类加载器初始化时,会调用 sun.misc.Launcher.getLauncher() ,即 Launcher.AppClassLoaderJava 中的系统类加载器。

类加载示例

类加载的日志开关,设置 JVM 参数:-verbose:class/-XX:+TraceClassLoading 来打印类加载过程。

子类引用父类静态字段

子类不会被初始化,而父类会被初始化。虽然虚拟机没有明确规定是否触发子类加载,但是当前测试虚拟机表现为:只要引用了,父类子类都会被加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestSubClassReferenceSuperStaticFiled {
private static class Super{
public static int value = 123;
static {
System.out.println("Super Class Init.");
}
}

private static class Sub extends Super{
static {
System.out.println("Sub Class Init.");
}
}

public static void main(String[] args) {
// SubClass not init.
System.out.println("Sub.value = " + Sub.value);
}
}

类加载过程及输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Opened /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
...
// 1. 父类、子类、测试类都被加载
[Loaded TestSubClassReferenceSuperStaticFiled from file:/home/xmt/test/classloading/]
[Loaded java.lang.Void from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded TestSubClassReferenceSuperStaticFiled$Super from file:/home/xmt/test/classloading/]
[Loaded TestSubClassReferenceSuperStaticFiled$Sub from file:/home/xmt/test/classloading/]
...
// 2. 父类初始化
Super Class Init.
// 3. 结果输出
Sub.value = 123
[Loaded java.lang.Shutdown from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]

测试结果可以看出,父类、子类、测试类都被加载,但是只执行了父类初始化阶段,子类没有被初始化。

数组定义的引用类

1
2
3
4
5
6
7
8
9
10
11
12
public class TestClassArray {
private static class Test{
static {
System.out.println("Test Class Init.");
}
}

public static void main(String[] args) {
Test[] tests = new Test[3];
System.out.println("Array length = " + tests.length);
}
}

类加载过程及输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
[Opened /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
...
// 1. 类加载阶段
[Loaded TestClassArray from file:/home/xmt/test/classloading/]
[Loaded java.lang.Void from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded TestClassArray$Test from file:/home/xmt/test/classloading/]
...
// 2. 结果输出
Array length = 3
[Loaded java.lang.Shutdown from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]

测试结果可以看出,只有类加载阶段,没有被初始化。

静态字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestStaticField {

private static class Test{
static final String STATIC_STR= "TestStaticFieldStr";
static final String STATIC_TIME = System.currentTimeMillis() + "";
static {
System.out.println("Test Class Init.");
}
}

public static void main(String[] args) throws InterruptedException {
// do not loading class
System.out.println("STATIC FINAL STR: " + Test.STATIC_STR);
System.out.println("***********************************");
// loading & init class: Test
System.out.println("STATIC FINAL TIME: " + Test.STATIC_TIME);
Thread.sleep(1000);
// STATIC_TIME value assigned when class loading.
System.out.println("STATIC FINAL TIME: " + Test.STATIC_TIME);
}
}

上面源码中,静态字段 STATIC_STR 是字符串常量;而 STATIC_TIME 虽然也是静态常量,但是需要在运行时才能确定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Opened /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
...
// 1. 加载测试类 TestStaticField
[Loaded TestStaticField from file:/home/xmt/test/classloading/]
...
// 2. 直接输出静态常量结果,没有执行 Test 类初始化,甚至类加载阶段都没有
STATIC FINAL STR: TestStaticFieldStr
***********************************
// 3. 加载类 Test
[Loaded TestStaticField$Test from file:/home/xmt/test/classloading/]
// 4. Test 类初始化,并在运行时对 STATIC_TIME 赋值
Test Class Init.
STATIC FINAL TIME: 1525837181577
// 5. STATIC_TIME 被 final 修饰,只会被赋值一次
STATIC FINAL TIME: 1525837181577
[Loaded java.lang.Shutdown from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
  • 静态字段 STATIC_STR
    编译时确定,属于常量,不会触发类初始化,甚至不会进入类加载阶段。
  • 静态字段 STATIC_TIME
    运行时确定,会触发类初始化并运行代码计算时间。STATIC_TIMEfinal 修饰,只会被赋值一次。

反射类加载和初始化不同阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestClassLoadingAndInit {

private static class Tester{
static {
System.out.println("Tester Class Init.");
}
}

private static class TesterForName{
static {
System.out.println("TesterForName Class Init.");
}
}

public static void main(String[] args) throws ClassNotFoundException {
String TesterClassName = "com.***.TestClassLoadingAndInit$Tester";
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
System.out.println("Test Class loading...");
classLoader.loadClass(TesterClassName);

System.out.println("**********************");
System.out.println("TesterForName Class loading and init.");
String testForName = "com.***.TestClassLoadingAndInit$TesterForName";
Class.forName(testForName);
}
}

类加载阶段、类初始化阶段的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Opened /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
...
// 1. 加载测试类 TestClassLoadingAndInit
[Loaded TestClassLoadingAndInit from file:/home/xmt/test/classloading/]
...
Tester Class loading...
// 2. ClassLoad.loadClass 仅仅会触发 Tester 类加载
[Loaded TestClassLoadingAndInit$Tester from file:/home/xmt/test/classloading/]
**********************
TesterForName Class loading and init.
// 3. Class.forName 会触发 TesterForName 类加载及类初始化
[Loaded TestClassLoadingAndInit$TesterForName from file:/home/xmt/test/classloading/]
TesterForName Class Init.
[Loaded java.lang.Shutdown from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]

反射过程中,这两个阶段分别由不同方法触发:

  • 类加载阶段
    ClassLoad.loadClass 仅仅会触发类加载阶段。
  • 类初始化阶段
    Class.forName 会触发类加载及类初始化阶段,默认使用调用类的类加载器进行类加载

类初始化顺序

虚拟机会确保子类初始化之前会执行父类初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestClassInitSeq {
private static class Father{
static {
value = 2;
// Illegal forward reference.
// System.out.println(value);
System.out.println("Father Class Init.");
}

static int value = 1;
}

private static class Son extends Father{
static int number = 3;
static {
System.out.println("Son Class Init.");
}
}

public static void main(String[] args) {
System.out.println("Son.number = " + Son.number);
}
}

在静态代码块中,只能对定义在之后的静态变量赋值,不能访问!否则会提示 Illegal forward reference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Opened /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
...
// 1. 加载测试类 TestClassInitSeq
[Loaded TestClassInitSeq from file:/home/xmt/test/classloading/]
[Loaded java.lang.Void from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
// 2. 先加载父类,再加载子类
[Loaded TestClassInitSeq$Father from file:/home/xmt/test/classloading/]
[Loaded TestClassInitSeq$Son from file:/home/xmt/test/classloading/]
...
// 3. 先初始化父类,再初始化子类
Father Class Init.
Son Class Init.
Son.number = 3
[Loaded java.lang.Shutdown from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]

父类 <clinit>() 方法会优先于子类 <clinit>() 方法执行。

类加载器默认属性和父加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestPropAndParent {
public static void main(String[] args) {
System.out.println("Boot: "
+ System.getProperty("sun.boot.class.path"));
System.out.println("Ext: " + System.getProperty("java.ext.dirs"));
System.out.println("App: " + System.getProperty("java.class.path"));

System.out.println("SystemLoader: "
+ ClassLoader.getSystemClassLoader());
System.out.println("Parent: "
+ ClassLoader.getSystemClassLoader().getParent());
System.out.println("GrandFather: "
+ ClassLoader.getSystemClassLoader().getParent().getParent());
}
}

运行结果:

1
2
3
4
5
6
7
xmt@server005:~/$ java TestProp
Boot: /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/resources.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/sunrsasign.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/jsse.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/jce.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/charsets.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rhino.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/jfr.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/classes
Ext: /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/ext:/usr/java/packages/lib/ext
App: .:/usr/lib/jvm/java-1.7.0-openjdk-amd64/lib/dt.jar:/usr/lib/jvm/java-1.7.0-openjdk-amd64/lib/tools.jar
SystemLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
Parent: sun.misc.Launcher$ExtClassLoader@75b84c92
GrandFather: null

系统默认的类加载器为 AppClassLoader,其父加载器为 ExtClassLoader。而 ExtClassLoader 的父加载器为 null ,虚拟机默认其为启动类加载器。

自定义类加载器

自定义加载器的主要步骤:

  • 获取字节数组
    读取本地 class 文件转换为数组,或者从网络等其他地方读取到二进制字节流并转换为数组。
  • defineClass 转换
    将字节数组转换为方法区的运行时数据结构。
  • 重写 loadClass/findClass 方法加载
    推荐重写 findClass 方法自定义加载类,loadClass 很容易破坏双亲委派模型。

重写 loadClass 方法

注意:在重写 loadClass 时,需要调用 super.loadClass 尽量保留双亲委派模型来处理父类。
对于任一个类:都需要由类加载器和这个类本身一同确立其在虚拟机中的唯一性。如下为自定义类加载器并验证 instanceof 判断类是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class TestClassInstanceOf {
public static void main(String[] args) throws Exception{
// 1. 自定义类加载器,重写 loadClass
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String s)
throws ClassNotFoundException {
try {
String fileName = s.substring(s.lastIndexOf(".") + 1)
+ ".class";
System.out.println("fileName = " + fileName);
// 增加堆栈打印 log,查看调用信息
new Exception("This is a log.").printStackTrace(System.out);
InputStream inputStream = getClass()
.getResourceAsStream(fileName);
// 2. 如果找不到 class 文件,交给父类执行双亲委派模型加载
if (inputStream == null) {
System.out.println("cant find class file: " + s);
return super.loadClass(s);
}
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
// 3. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
System.out.println("defineClass.");
Class clazz = defineClass(s, data, 0, data.length);
System.out.println("clazz = " + clazz);
return clazz;
}catch (IOException e){
throw new ClassNotFoundException(s);
}
}
};

// 4. 使用自定义类加载器加载
Class clazz = classLoader.loadClass("com.***.TestClassInstanceOf");
System.out.println("clazz loader: " + clazz.getClassLoader());
Object object = clazz.newInstance();
// 5. 打印当前自定义类加载器加载的类
System.out.println("object = " + clazz.getClass());
// 判断和系统加载器加载的是否为同一个类
System.out.println("the same class but two different classLoaders, "
+ " instanceof = " + (object instanceof TestClassInstanceOf));
}
}

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
[Opened /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
...
// 1. 系统加载器加载: TestClassInstanceOf 和自定义类加载器
[Loaded TestClassInstanceOf from file:/home/xmt/test/classloading/]
[Loaded TestClassInstanceOf$1 from file:/home/xmt/test/classloading/]
...
// 2. 自定义类加载器开始加载:TestClassInstanceOf
fileName = TestClassInstanceOf.class
[Loaded java.lang.Throwable$PrintStreamOrWriter from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
...
java.lang.Exception: This is a log.
at TestClassInstanceOf$1.loadClass(TestClassInstanceOf.java:12)
at TestClassInstanceOf.main(TestClassInstanceOf.java:30)
// 3. defineClass 中会递归加载 TestClassInstanceOf 的父类 Object
// 自定义加载器中,父类由双亲委派模型处理
fileName = Object.class
java.lang.Exception: This is a log.
at TestClassInstanceOf$1.loadClass(TestClassInstanceOf.java:12)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:800)
at java.lang.ClassLoader.defineClass(ClassLoader.java:643)
at TestClassInstanceOf$1.loadClass(TestClassInstanceOf.java:21)
at TestClassInstanceOf.main(TestClassInstanceOf.java:30)
cant find class file: java.lang.Object
// 4. 自定义类加载器:从虚拟机中成功加载 TestClassInstanceOf
// 将字节文件转换为方法区的运行时数据结构
[Loaded TestClassInstanceOf from __JVM_DefineClass__]
clazz = class TestClassInstanceOf
// 5. 对类 TestClassInstanceOf 进行反射调用时,触发类加载过程的类初始化阶段
// 初始化阶段:如果类没有初始化,需要先触发类初始化;如果父类没有初始化,先触发父类初始化
// 这里触发的是自定义类加载器自身及其父类 ClassLoad 的类初始化
// 父类 ClassLoad 类加载由双亲委派模型去处理
fileName = ClassLoader.class
java.lang.Exception: This is a log.
at TestClassInstanceOf$1.loadClass(TestClassInstanceOf.java:12)
at java.lang.Class.getDeclaredConstructors0(Native Method)
at java.lang.Class.privateGetDeclaredConstructors(Class.java:2595)
at java.lang.Class.getConstructor0(Class.java:2895)
at java.lang.Class.newInstance(Class.java:354)
at TestClassInstanceOf.main(TestClassInstanceOf.java:32)
cant find class file: java.lang.ClassLoader
// 6. 自定义类加载器开始加载自己: TestClassInstanceOf$1
fileName = TestClassInstanceOf$1.class
java.lang.Exception: This is a log.
at TestClassInstanceOf$1.loadClass(TestClassInstanceOf.java:12)
at java.lang.Class.getDeclaredConstructors0(Native Method)
at java.lang.Class.privateGetDeclaredConstructors(Class.java:2595)
at java.lang.Class.getConstructor0(Class.java:2895)
at java.lang.Class.newInstance(Class.java:354)
at TestClassInstanceOf.main(TestClassInstanceOf.java:32)
defineClass.
// 7. 自定义类加载器:从虚拟机中成功加载自己 TestClassInstanceOf$1
[Loaded TestClassInstanceOf$1 from __JVM_DefineClass__]
clazz = class TestClassInstanceOf$1
// 8. 使用 instanceof 比较,同一个类不同类加载器加载后,并不相等
object = class TestClassInstanceOf
the same class but two different classLoaders, instanceof = false
[Loaded java.lang.Shutdown from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar]

重写 loadClass 方法的注意事项:

  • 如果找不到指定 class 文件,调用父类方法处理
    当找不到指定 class 文件时,需要父类方法的双亲委派模型处理,比如 defineClass 会加载父类,而很多基础父类组件如 Object 是三大父加载器加载的。
  • 被加载类 class 文件位置
    因为重写 loadClass 可以直接避开双亲委派模型,所以任意位置的 class 文件都可以正确解析。

疑问:自定义类加载器为什么要加载自己 TestClassInstanceOf$1 ? 如果将自定义类加载器改为 static class 静态内部类,则不会加载自己。反编译 TestClassInstanceOf$1.class 文件,可以发现 static 方法内的匿名内部类,反编译后是一个 final class,而且和静态内部类一样,不会引用外部类 this

重写 findClass 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 被加载测试类,它的 class 文件不能放在:自定义类加载器文件的当前目录及其子目录中
// 否则会因为双亲委派模型,被系统加载器扫描到并加载
// 当前示例放在 javabase/src/main/resources/ 目录下
public class Test {
static {
System.out.println("Test.class file is loaded by: "
+ Test.class.getClassLoader());
}

public void test(){
System.out.println("It is Test::test.");
}
}

// 自定义类加载器
public class MyClassLoader extends ClassLoader{

// Test.class file dir
// 指定被加载 class 文件的路径
private String mClassFileDir;

public MyClassLoader(String classFileDir) {
mClassFileDir = classFileDir;
}

@Override
protected Class<?> findClass(String s) throws ClassNotFoundException {
System.out.println("MyClassLoader::findClass");
Class clazz = null;
try {
// 根据类全名查找对应的 class 文件,读取后转化为数组
byte[] data = loadClassData(s);
if (data != null) {
// 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
clazz = defineClass(s, data, 0, data.length);
}
} catch (IOException e){
e.printStackTrace();
}

if (clazz == null){
throw new ClassNotFoundException(s);
}

System.out.println("MyClassLoader::findClass, clazz = " + clazz);
return clazz;
}

private byte[] loadClassData(String s) throws IOException {
// 判断给定的 class 文件目录,及对应的 class 文件是否存在
File dir = new File(mClassFileDir);
if (!dir.exists()){
throw new IOException("mClassFileDir is not exists.");
}

String fileName = s + ".class";
File classFile = new File(dir.getAbsolutePath(), fileName);

if (!classFile.exists()){
throw new IOException(fileName + " file is not exists.");
}

System.out.println("loadClassData: classFilePath = "
+ classFile.getAbsolutePath());

// 如果 class 文件存在,读取到数组中
byte[] data = null;
FileInputStream fileInputStream = new FileInputStream(classFile);
ByteArrayOutputStream byteArrayOutputStream =
new ByteArrayOutputStream();
int count = 0;
while ((count = fileInputStream.read()) != -1) {
byteArrayOutputStream.write(count);
}
data = byteArrayOutputStream.toByteArray();

return data;
}
}

// 测试自定义类加载器
public class TestCustomClassLoader {

public static void main(String[] args) {
// 指定 class 文件所在目录
String classFileDir = "javabase/src/main/resources/";
MyClassLoader myClassLoader = new MyClassLoader(classFileDir);
try {
// 使用自定义类加载器加载,并实例化
Class testReflect = myClassLoader.loadClass("Test");
Object object = testReflect.newInstance();
Method method = testReflect.getMethod("test", null);
// 调用被加载类的测试方法
method.invoke(object, null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}

// 输出结果
MyClassLoader::findClass
loadClassData: classFilePath = C:\Users\xmt\***\src\main\resources\Test.class
MyClassLoader::findClass, clazz = class Test
Test.class file is loaded by: com.***.classloading.MyClassLoader@6bc7c054
It is Test::test.

重写 findClass 方法的注意事项:

  • 被加载类 class 文件位置
    被加载类 Test.class 文件,不能放在 MyClassLoader 文件所在目录及其子目录中。重写 findClass 保留了双亲委派模型,会被 AppClassLoader 系统加载器扫描到并加载,自定义加载器无法生效。
    Android Studio 中,这个 class 文件甚至不能直接由 AS 生成。打印 AppClassLoader 加载路径为:...;C:\Users\xmt\AndroidStudioProjects\xmt\gitlab\04_androidbasic\android_basic_knowledge\javabase\build\classes\java\main;...,也就是说,所有由 AS 工具生成的 class 文件都会默认被系统加载器加载。
  • 自定义加载器
    判断传入的 class 文件目录及对应文件是否存在,如果存在则先转换为数组,并通过 defineClass 将二进制文件转换为运行时数据结构。

后续

  • 线程上下文加载器
  • Android 类加载机制

参考文档