Java类加载机制

类加载器


类加载器 classLoader 加载 Java 类到 JVM 中,JVM 使用 Java类的方式如下:

Java 源程序 (.java 文件)经过编译器编译之后被转为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节码,转换成 java.lang.Class 类的一个实例。 每个这样的实例用来表示一个 Java 类,通过 newInstance() 方法就可以创建出该类的一个对象,但是很多情况下,Java 字节码可能是通过工具动态生成的,也可能是通过网络下载的。

java.lang.ClassLoader 类


java.lang.ClassLoader 的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即一个 java.lang.ClassLoader 的一个实例,除此之外,ClassLoader 还负责加载 Java 应用所需的资源,比如图像文件等等,为了完成加载类的职责,ClassLoader 提供了一系列方法:

getParent() 返回该类加载器的父类加载器
loadClass(String name)加载名称为 name 的类,返回结果是 java.lang.Class 类的实例
findClass(String name)查找名为 name 的类,返回 java.lang.Class 类的实例
findLoadedClass(String name) 查找名为 name 的已经被加载过的类,返回结果是 java.lang.Class 类的实例
defineClass(String name, byte[] b, int off, int len) 把字节数组 b 中的内容转换成 Java 类, 返回结果是一个 java.lang.Class 类的实例,这个方法被生命为 final
resolveClass(Class<?> c) 链接指定的 Java 类。

类加载器的树状组织结构


类加载器大致分为两类,系统和 Java 应用开发人员编写

  • 引导类加载器(bootstrap class loader):加载 Java 核心库,原生代码实现,不继承自 java.lang.ClassLoader
  • 扩展类加载器(extensions class loader):加载 Java 扩展库,JVM 的实现会提供一个扩展库,类加载器在此查找并加载 Java 类。
  • 系统加载类(system class loader):根据 Java 应用的类路径 (ClassPath) 加载 Java 类,Java 应用的类都是由它来完成加载的,可以通过 ClassLoader.getSystemClassLoader() 来获取它。

除了以上,所有类加载器都有一个父类加载器,通过给出的 getParent() 方法可以得到。

系统类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是引导类加载器。对于开发人员编写的类加载器来说, 其父类的类加载器加载该类的类加载器,类加载器类和其他 Java 类一样,都是要由类加载器来加载,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。

1
2
3
4
5
6
7
8
9
10
11
public class ClassLoaderTest {
public static void main(Stringp[] args) {
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString);
loader = loader.getParent(); // 树状结构
// sun.misc.Launcher$AppClassLoader@2a139a55
// sun.misc.Launcher$ExtClassLoader@7852e922
}
}
}

每个 Java 类都有一个 指向定义它 的类加载器的指针, 通过 getClassLoader() 方法可以获取到引用,通过递归调用 getParent() 方法输出全部的父类加载器。

上述代码,第一个输出的是 ClassLoaderTree 类的类加载器,即系统类加载器,它是 sum.misc.Launcher$AppClassLoader 的实例,第二个输出的是扩展类加载器,是 sum.misc.Launcher$ExtClassLoader 的实例,值得注意的是 这里并没有输出引导类加载器,这是因为 JDK 的实现对于父类加载器是引导类加载器的情况, getParent() 方法返回 null。

类加载器的代理模式


首先需要说明,JVM 是如何判断两个 Java 类是相同的,JVM 不仅要看类的全名是否相同,还要看此类的类加载器是否一样。

两者都相同,说明两个类才是相同的。即便是相同的字节代码,被不同的类加载器加载后所得到的类,也是不同的。如果将两个不同的类强行赋值,就会抛 ClassCastException。

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
public class Sample {
private Sample instance;

public void setSample(Object instance) {
this.instance = (Sample) instance;
}

@Test
public void testClassIdentity() throws Exception {
String classDataRootPath = "classData";
// 创建两个类加载器对象
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.lizhaoloveit._ClassLoader.Sample";
// 加载同一份字节码
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();

Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
// 调用字节码的方法时,会报错 ClassCastException,因为类加载器不同,创建的字节码对象不是同一个
setSampleMethod.invoke(obj1, obj2);
}
}

类加载器在尝试自己去查找某个类的字节码并调用 defineClass() 方法之前,会先让其父类加载器尝试加载,以此类推。

代理模式是为了保证 Java 核心库的类型安全,所有 Java 都至少需要引用 java.lang.Object 类,运行的时候,java.lang.Object 这个类需要被加载到 JVM 中,如果加载过程由自己的类加载器完成的话,JVM 中就会存在多个 java.lang.Object 字节码对象,互相之间不兼容。通过代理模式, Java 核心库的类加载工作由引导类加载器统一完成,保证 Java 应用所使用的类都是同一个版本的核心库的类。

而不同的类加载器又为相同名称的类创建了额外的名称控件,相同名称的类可以存在于 JVM 中,只需要不同的类加载器来加载她们即可,就相当于在 JVM 内部创建了一个个相互隔离的 Java 类空间。(项目和项目之间可以有 权限名相同的类,引导类加载器不同)

加载类的过程


类加载器在查找某个类的字节码并调用defineClass()它之前,会先让父类加载器尝试加载这个类。意味着完成类的加载工作的类和启动这个加载过程的类加载器有可能不是同一个。真正的完成类加载工作是通过调用defineClass()来实现。启动加载过程是通过调用 loadClass() 来实现。前者叫做一个类的定义加载器(defining loader),后者叫做初始加载器(initiating loader)。

JVM 判断两个类是否相同使用的是 defining loader,也就是说谁启动加载过程不重要,两种类加载器的关联在于:一个类的定义加载器,是它所引用的类的初始加载器,如 com.example.Outer 类 引用了 com.example.Inner 类,所以在 com.example.Outer 类加载到 JVM 时,发现该类还引用了 com.example.Inner 类,所以会启动 com.example.Inner 类的加载程序,然后让其父类去尝试加载这个类。

类加载器在成功加载某个类后,会把得到的 java.lang.Class 实例缓存起来,下次再请求加载该类的之前(loadClass()之前),会直接使用缓存的类的实例,而不会尝试再次加载。

假设项目中我们有自己的类加载器,LZClassLoader,继承自 ClassLoader

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
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 {
// 如果不是扩展类加载器,则调用父类加载器的 loadClass 方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// Bootstrap 是C++的类加载器,主要用来加载 java的内部核心类的,即权限名以 java 开头的一些类,由 sun 公司编写的,如果发现没有则返回空。
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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

模拟类加载过程


Test类 引用 Persion类 name 为类的权限名

JVM 要加载 Test类和 Person类的过程:

  1. LZClassLoader 要加载 Test类 时会调用 loadClass(name)loadClass(name) 内部会调用 findLoadedClass(name) 判断是否被加载过,如果没有被加载过,会调用 LZClassLoader 的父类加载器对象的loadClass(name) 方法,同样的,LZClassLoader 的父类加载器的 loadClass(name) 方法中也做了同样的事情。
  2. 在 Test类 的定义加载器调用 defineClass() 时,发现了 Test类 引用了 Person类 ,则 Test类 的定义加载器会调用 loadClass() 方法加载 Person,loadClass()方法所做的事情和加载 Test类 相同 (Test类 的定义加载器有可能是 LZClassLoader)。
  3. Test类 一旦被类加载器对象加载,类加载器对象就会引用该类的字节码对象。findLoadedClass() 方法就是寻找加载器对象是否加载过 Test类。

这里有个问题:万一 Person类 的类加载器是 Test类 的定义加载器的子类怎么办?即所有的 Test类 的类加载器和其所有父类类加载器都无法加载 Person类 怎么办?
还有其他方法可以来解决这种问题,比如线程上下文加载器。

线程上下文类加载器(context class loader)


java.lang.Thread 中的方法 getContextClassLoader()setContextClassLoader(ClassLoader c) 用来获取和设置线程上线文类加载器,如果没有设置,线程将继承父线程的类加载器。

Service Provider Interface : Java 在语言层面为我们提供了可扩展应用的途经,SPI 提供了一种 JVM 级别的服务发现机制,我们只需要按照 SPI 的要求,在 jar 包中进行适当的配置,JVM 就会在运行时通过懒加载,帮我们找到所需的服务并加载。常见的 SPI 有 JDBC、JCE、JNDI、JAXP、JBI 等。

context class loader 可以解决的问题:

Java 提供了很多服务接口(SPI),允许第三方为这些接口提供实现。这些 SPI 由 Java 核心库提供,如 JAXP 的 SPI 定义包含在 javax.xml.parsers 包中,但是 SPI 的实现代码很可能是作为 Java 应用依赖的 jar 包被包含进来,可以通过 CLASSPATH 找到路径,比如 JAXP SPI 的 Apache Xerces 包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。 如 JAXP 的 javax.xml.parsers.DocumentBuilderFactory 类中的 newInstance() 方法生成一个新的 DocumentBuilderFactory 的实例。实例的真正类是 继承 javax.xml.parsers.DocumentBuilderFactory 由 SPI 的实现提供的。在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。

问题来了, SPI 的接口是 Java 核心库提供的,核心库的类加载器是引导类加载器,而 SPI 的实现类一般是由系统类加载器来加载的,引导类加载器无法找到 SPI 的实现类,按照代理的模式,加载引用类时,只能往上找。

Java 应用的线程的上下文类加载器默认是系统上下文类加载器,在 SPI 接口的代码中使用线程上下文类加载器, 就可以成功的加载到 SPI 实现类。

Class.forName


Class.forName 是静态方法,同样可以加载类,常用 API

  • Class.forName(String name, boolean initialize, ClassLoader loader)
  • Clas.forName(String classname)

initialize 表示是否初始化类,loader 表示加载时使用的类加载器。

第二种形式,默认初始化,并且用当前类的类加载器进行加载。

用法:常见的用法是加载数据库驱动,如 Class.forName(“org.apache.derby.jdbc.EmbeddedDriver”).newInstance() 用来加载 Apache Derby 数据库的驱动

开发自己的类加载器


用处:某些情况下,比如你需要通过网络来传输 Java 类的字节码,为了保证安全,需要对字节码加密,这时,需要自己的类加载器来从某个网络地址上读取加密后的字节码,然后进行解密和验证。

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
public class FileSystemClassLoader extends ClassLoader { 

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}

类 FileSystemClasLoader 集成 java.lang.ClassLoader。 一般来说,我们自定义的类加载器只需要覆写 findClass(String name) 即可,

为什么我们只需要覆写 findClass()
ClassLoader 会调用 loadClass() 去加载一个类,而 loadClass() 内部会调用 findLoadedClass() 方法来找出是否加载过该类,然后又会调用父类类加载器的 loadClass() 方法,如果所有类加载器都没有找到该类的话,就会调用 findClass() 方法来查找此类,因此为了保证 类加载器正确实现代理方法,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass() 方法。

类 FileSystemClassLoader 的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2019/07/01/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论