谭浩的博客

Simple is beauty.

Java 的类加载机制

基础介绍

类加载器负责在运行时将 Java 类地加载到 JVM 中,其是 JRE 的一部分。这些 Java 类不会同时全部被加载到内存中,而是在应用需要时才会加载。

内置的类加载器类型

Bootstrap Class Loader:

Bootstrap Class Loader由本地代码编写而不是使用Java编写,它没有任何父类。所以在打印其内容时,会输出 null。而不同的 JVM 实现的 Bootstrap Class Loader 也不一样。

我们知道Java 类由一个 java.lang.ClassLoader 实例加载进 JVM,而类加载器也是类本身,那么由谁去加载java.lang.ClassLoader类呢?Bootstrap Class Loader就是负责加载JDK内部类的,比如说 rt.jar 以及其他在 $JAVA_HOME/jre/lib中的库文件。Bootstrap Class Loader也是其他所有类加载器的父类。

Extension Class Loader:

标准扩展类加载器是 Bootstrap 类加载器的子类,其用于加载 Java 的标准扩展类。标准扩展类通常位于$JAVA_HOME/lib/ext目录下或者由系统属性 java.ext.dirs定义。

System Class Loader:

系统或者应用类加载器,用于加载所有应用级别的类到 JVM 中。它加载在类路径环境变量(-classpath 或者 -cp 命令行选项)中的类。它是标准扩展类加载器的子类。除了Bootstrap Class Loader 所有的类加载器都是使用java.lang.ClassLoader实现的。

类加载器层次

类加载器的工作原理

类加载器是 JRE 的一部分,当 JVM 请求一个类时,类加载器尝试在运行时使用全限定类名去定位以及加载类。

双亲委派模型

类加载器工作原理

类加载器使用双亲委派模型,当一个类或者资源的请求到达时,系统类加载器的实例会将该请求委托给父类加载器,并递归委托。

例如假设我们请求一个应用类,首先加载类的请求会交给系统类加载器,其将请求委托给它的父类标准扩展类加载器,而标准扩展类加载器则进一步将请求委托给引导类加载器。

引导类加载器将在rt.jar 寻找类,如果没有找到则标准扩展类加载器将在jre/lib/ext中定位类文件,如果依然没找到,则有应用类加载器在类路径中查找类文件。如果任意一个父类加载了类,则子类将不再加载该类。

唯一性

因为双亲委派模型,被父类加载的类将不会被子类再次加载,其保证了类唯一。

可见性

子类加载器加载的类可以看见父类加载器加载的类。例如系统类加载器加载的类可以访问由标准扩展类加载器以及Bootstrap类加载器加载的类,但反之不行。

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
public class ClassLoaderTest {

public static void main(String args[]) {
try {
System.out.println("ClassLoaderTest.getClass().getClassLoader() : "
+ ClassLoaderTest.class.getClassLoader());


Class.forName("me.tanyihao.ClassLoaderTest", true
, ClassLoaderTest.class.getClassLoader().getParent());
} catch (ClassNotFoundException ex) {
Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex);
}
}

}
/* output
ClassLoaderTest.getClass().getClassLoader() : jdk.internal.loader.ClassLoaders$AppClassLoader@4459eb14
Mar 09, 2019 4:54:12 PM me.tanyihao.ClassLoaderTest main
SEVERE: null
java.lang.ClassNotFoundException: me.tanyihao.ClassLoaderTest
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:374)
at me.tanyihao.ClassLoaderTest.main(ClassLoaderTest.java:23)
*/

自定义类加载器

内置的类加载器已经满足了大部分需要加载类的情况,然而在一些需要在外部存储硬盘以及网络上加载所需的类时就需要自定义类加载器。

自定义类加载器案例

  1. 帮助修改现存的字节码,例如:织入
  2. 动态创建用户需要的类,例如:JDBC,通过动态类加载切换不同的驱动实现
  3. 实现类版本控制机制, 同时为具有相同名称和包的类加载不同的字节码

创建一个自定义类加载器

我们需要继承 ClassLoader类并重写 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
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public class CustomClassLoader extends ClassLoader {

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = new byte[0];
try {
b = loadClassFromFile(name);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name, b, 0, b.length);
}

private byte[] loadClassFromFile(String name) throws IOException {
InputStream is = getClass().getClassLoader().getResourceAsStream(
name.replace('.', File.separatorChar)+".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int value = 0;
while ((value = is.read()) != -1) {
byteStream.write(value);
}
buffer = byteStream.toByteArray();
return buffer;
}
}

理解类加载器

loadClass()

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException

该方法按照全限定名加载一个类,如果resolve设置为true则解析类,如果我们只需要去判断一个类是否存在则可以将resolve参数设置为 false。该方法是类加载器的入口。其源码如下:

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
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) {
// 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
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

类的搜索需要以下步骤:

  1. 调用findLoadedClass(String)方法查看类是否已经加载
  2. 调用父类的loadClass(String)方法
  3. 如果父类没能加载该类,则调用findClass(String)加载类

defineClass()

protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError

在使用类之前,需要解析类,该方法用于将字节数组转化为一个类的实例。

findClass()

protected Class<?> findClass(String name) throws ClassNotFoundException

该方法按照全限定名查找类,在自定义类中我们需要重写该方法。

getParent()

public final ClassLoader getParent()返回父类类加载器

getResource()

public URL getResource(String name)该方法按照给定名寻找资源,它首先委托父类加载器寻找。如果失败,该方法将调用findResource(String)去寻找资源。输入路径可以类路径的相对路径和绝对路径。

上下文类加载器

上下文类加载器为双亲委派模型提供了另一种类加载方式。正如我们上文提到的,JVM 中的类加载器遵从一个层次模型,除了引导类加载器每一个类加载器都有一个父类。

然而,有时候 JVM 核心类需要去加载一个开发者提供的类和资源,这时我们就遇到了大麻烦。例如,JNDI 的核心功能是在rt.jar中实现。但是JNDI类需要加载由独立供应商实现的JNDI程序(部署在应用程序类路径中)。由Bootstrap类加载器加载的类访问由应用类加载器加载的类。

双亲委派模型无法解决此类问题,我们需要找到一种可用的方式加载类。因此可以用线程上下文类加载器实现。java.lang.Thread类具有getContextClassLoader()方法用于返回一个特定线程的上下文类加载器,该上下文类加载器可以用于加载类和资源。

一些问题

  1. 什么是类加载器?JVM 中有哪些类加载器?

    JVM 用于加载类和接口,JVM 中有Bootstrap、Extension、System类加载器。

  2. loadClass 和 Class.forName 的区别?

    loadClass 只用于加载类但是不初始化对象,Class.forName 在加载之后会初始化对象。

  3. JVM 如何加载类?

    类加载器是分层的。第一个类是在类中声明的静态 main () 方法的帮助下专门加载的。所有随后加载的类都由已加载并运行的类加载。而具体的类加载使用双亲委派模型。

  4. 动态类加载和静态类加载的区别?

    静态类加载是由 Java 的new操作加载的,动态类加载则使用编程的方式加载,例如使用Class.forName(className)loadClass()