JVM简明笔记3:类加载机制

1 类的加载

类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。

类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

类加载器并不需要等到某个类被首次主动使用时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

加载 .class 文件的方式:

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载 .class 文件
  • 从专有数据库中提取 .class 文件
  • 将Java源文件动态编译为 .class 文件

执行顺序

  • 类加载子系统负责从文件系统或是网络中加载 .class 文件,class文件在文件开头有特定的文件标识。
  • 把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
  • 如果调用构造器实例化对象,则该对象存放在堆区;

2 类的生命周期

  • 加载(Loading)
  • 连接(Linking)
    • 验证(Verification)
    • 准备(Preparation)
    • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

加载阶段(Loading)

此阶段用于查到相应的类(通过类名进行查找)并将此类的字节流转换为方法区运行时的数据结构,然后再在内存中生成一个能代表此类的 java.lang.Class 对象,作为其他数据访问的入口。

(需要注意的是加载阶段和连接阶段的部分动作有可能是交叉执行的,比如一部分字节码文件格式的验证,在加载阶段还未完成时就已经开始验证了。)

  • 预加载:虚拟机启动时加载,加载的是 JAVA_HOME/lib/ 下的 rt.jar 下的 .class 文件 (可以写一个空的 main 函数,设置虚拟机参数为 -XX:+TraceClassLoading 来获取类加载信息)
  • 运行时加载:虚拟机在用到一个 .class 文件的时候,会先去内存中查看一下这个 .class 文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。
    • 获取 .class 文件的二进制流 (没有规定二进制字节流要必须从哪里来或者怎么来,所以留下了可扩展的空间)
    • 将类信息、静态变量、字节码、常量这些 .class 文件中的内容放入方法区中
    • 在内存中生成一个代表这个 .class 文件的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的

验证阶段(Verification)

此步骤主要是为了验证字节码的安全性,如果不做安全校验的话可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是 JVM 自我保护的一项重要举措。

验证的主要动作大概有以下几个:

  • 文件格式校验包括常量池中的常量类型、Class 文件的各个部分是否被删除或被追加了其他信息等;
  • 元数据校验包括父类正确性校验(检查父类是否有被 final 修饰)、抽象类校验等;
  • 字节码校验,此步骤最为关键和复杂,主要用于校验程序中的语义是否合法且符合逻辑;
  • 符号引用校验,对类自身以外比如常量池中的各种符号引用的信息进行匹配性校验。

准备阶段(Preparation)

此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上。

HotSpot 虚拟机在 JDK 1.7 之前都在方法区,而 JDK 1.8 之后此变量会随着类对象一起存放到 Java 堆中。

  • 这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
  • 这个阶段赋初始值的变量指的是那些不被 final 修饰的 static 变量
    • 比如 public static int value = 123,在准备阶段过后是 0 而不是 123 ,给 value 赋值为123的动作将在初始化阶段才进行
    • 比如 public static final int value = 123; ,在准备阶段,虚拟机就会给 value 赋值为123。

解析阶段(Resolution)

此阶段主要是用来解析类、接口、字段及方法的,解析时会把符号引用替换成直接引用。

  • 符号引用:是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;
  • 直接引用:是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。

初始化(Initialization)

初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。

3 Java类加载器

  • 启动类加载器(BootStrapClassLoader):C/C++ 实现
  • 其他类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader):Java 实现,规范定义自定义加载器是指派生于抽象类 ClassLoder 的类加载器

启动类加载器(Bootstrap ClassLoader)

C/C++ 实现,嵌套在 JVM 中,并不继承自 java.lang.ClassLoader,没有父加载器。用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar 或 sun.boot.class.path 路径下的内容),用于提供JVM自身需要的类。

扩展类加载器(Extension ClassLoader)

Java 实现,派生于 ClassLoader。由 sun.misc.Launcher$ExtClassLoader 实现。用来从 java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载。

应用类加载器(Application ClassLoader),又称系统类加载器

Java 实现,派生于 ClassLoader,父类加载器为扩展类加载器。由 sun.misc.Lanucher$AppClassLoader 实现。程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载的,它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库。通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。

用户自定义类加载器(User-Defined ClassLoader)

在日常的 Java 开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的加载方式。

类加载器包含示意图

ClassLoader 相关类图

重点关注 ClassLoader、ExtClassLoader、AppClassLoader。

4 双亲委派模型

过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载。

好处:使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。同时也避免了多份同样字节码的加载。

Java 中实现双亲委派的代码都集中在 java.lang.ClassLoaderloadClass() 方法中,简单分析如下:

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
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, 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. 判断一下是否有父加载器,若有交给父加载器加,否则调用 BootstrapClassLoader 加载。
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) { // 3. 如果第二步骤依然没有找到指定的类,那么调用当前类加载器的 findClass 方法来完成类加载。
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
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;
}
}

// 自定义类加载器就可以重写 finalClass 方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

我们分析下 ExtClassLoader 与 AppClassLoader 源码,这两个类都是 sun.misc.Launcher 类的静态内部类

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/**
* This class is used by the system to launch the main application. Launcher类
*/
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
private static Launcher launcher = new Launcher(); // 1. Luncher 类加载后会从此静态变量调用到默认的构造方法
private static String bootClassPath =
System.getProperty("sun.boot.class.path");

public static Launcher getLauncher() {
return launcher;
}

private ClassLoader loader;

public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader(); // 2. 扩展类加载器
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}

// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl); // 3. 应用类加载器(系统类加载器)
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}

// Also set the context class loader for the primordial thread. // 4. 设置 ContextClassLoader 为应用类加载器(系统类加载器)
Thread.currentThread().setContextClassLoader(loader);

// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
// init FileSystem machinery before SecurityManager installation
sun.nio.fs.DefaultFileSystemProvider.create();

SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
}

// ...


/*
* The class loader used for loading installed extensions. 关于 ExtClassLoader
*/
static class ExtClassLoader extends URLClassLoader {

// ...

private static ExtClassLoader createExtClassLoader() throws IOException {
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().

return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
final File[] dirs = getExtDirs(); // 4. 获取扩展类加载器加载的目录文件
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs); // 5. 创建一个扩展类加载器进行返回
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}

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

/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
SharedSecrets.getJavaNetAccess().
getURLClassPath(this).initLookupCache(this);
}

private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs"); // 1. 将加载变量 java.ext.dirs 的值指示的路径下的类
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator); // 2. 按照 File.pathSeparator 进行 split
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs; // 3. 返回 File 对象数组
}


// ...

}

/**
* The class loader used for loading from java.class.path. 关于 AppClassLoader
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {

// ...

public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path"); // 1. 加载 java.class.path
final File[] path = (s == null) ? new File[0] : getClassPath(s);

// Note: on bugid 4256530
// Prior implementations of this doPrivileged() block supplied
// a rather restrictive ACC via a call to the private method
// AppClassLoader.getContext(). This proved overly restrictive
// when loading classes. Specifically it prevent
// accessClassInPackage.sun.* grants from being honored.
//
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}

final URLClassPath ucp;

/*
* Creates a new AppClassLoader
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
ucp.initLookupCache(this);
}

/**
* Override loadClass so we can checkPackageAccess. // 0. 重写 loadClass 方法:checkPackageAccess 之后调用 super.loadClass
*/
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}

if (ucp.knownToNotExist(name)) {
// The class of the given name is not found in the parent
// class loader as well as its local URLClassPath.
// Check if this class has already been defined dynamically;
// if so, return the loaded class; otherwise, skip the parent
// delegation and findClass.
Class<?> c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
throw new ClassNotFoundException(name);
}

return (super.loadClass(name, resolve));
}

// ...
}
}

5 自定义类加载器

自定义类加载器的目的:

  • 隔离加载类:模块隔离,把类加载到不同的应用选中。比如 Tomcat 这类 Web 应用服务器,内部自定义了好几中类加载器,用于隔离 Web 应用服务器上的不同应用程序。
  • 修改类加载方式:除了 Bootstrap 加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。
  • 扩展加载源:可以实现从其他途径加载 class 文件。
  • 防止源码泄漏:Java 代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。

通过对双亲委派模型源码的解读,我们可以分析出两种自定义类加载器的做法:

  • 重写 loadClass 方法:不推荐,因为会破坏双亲委派模型
  • 重写 findClass 方法:推荐

采用 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
public class MyClassLoader extends ClassLoader{

private String dir;
public static final String fileType = ".class";

public MyClassLoader(String dir) {
this.dir = dir;
}

public MyClassLoader(ClassLoader parent, String dir) {
super(parent);
this.dir = dir;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 定义输入和输出流
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;

String fileName = dir + name + fileType;
try {
bis = new BufferedInputStream(new FileInputStream(fileName));
baos = new ByteArrayOutputStream();

// 读取字节数据
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
byte[] byteCode = baos.toByteArray();

// 字节数组转为Class对象
Class<?> definedClass = defineClass(null, byteCode, 0, byteCode.length);
return definedClass;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}

return null;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClassLoaderTest {

public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader("D:/");
try {
Class<?> dogClass = classLoader.loadClass("Test");
System.out.println(dogClass.getClassLoader().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

内容为之前学习笔记整理,如果有问题请指正!