logo头像

读万卷书,行万里路

Java中类加载器classloader浅析

本篇文章来简析一下 classloader 在 Java 中的应用。

前言

现在一般一个应用程序开发会包含很多很多的类, Java 程序启动时并不是一次性将所有的类全部加载到内存中进行运行的,而是先加载部分的类到 JVM 中,然后等 JVM 需要用到其他的类时再加载进去,这样的好处就是节约内存,提高了效率。

在 Java 中类加载器就是 ClassLoader , ClassLoader 的具体作用就是将 class 文件加载到 jvm 虚拟机中去,程序就可以正确运行了。

Class 再认识

我们平常写的 Java 文件的格式是 xxx.java 文件格式的,这个格式并不是 JVM 执行的格式, JVM 执行的是 .class 格式的文件,这就需要将 .java 的格式文件转为 .class 的格式,这就是编译过程。命令是:

1
javac HelloWorld.java

通过 javac 命令即可在当前目录下面生成 .class 文件,这个文件就是 JVM 能够执行的文件格式。

在这里插入图片描述

接着通过 java 命令即可运行这个文件:

1
java HelloWorld

打印结果如下所示:
在这里插入图片描述
以上便是一个完整的 Java 文件运行过程,先经过编译将 .java 文件转为 .class 格式以便 JVM能够执行。接下来我们来详细分析。

Java 类加载器

ClassLoader 分类

每个 ClassLoader 对象都是一个 java.lang.ClassLoader 的实例。每个Class对象都被这些 ClassLoader 对象所加载,通过继承java.lang.ClassLoader 可以扩展出自定义 ClassLoader,并使用这些自定义的 ClassLoader 对类进行加载。

1
2
3
4
5
6
7
8
9
10
package java.lang;
public abstract class ClassLoader {
public Class loadClass(String name);
protected Class defineClass(byte[] b);
public URL getResource(String name);
public Enumeration getResources(String name);
public ClassLoader getParent();
Class<?> findClass(String name)
//...
}

1,loadClass

它接受一个全类名,然后返回一个 Class 类型的实例。

2,defineClass

方法接受一组字节,然后将其具体化为一个 Class 类型实例,它一般从磁盘上加载一个文件,然后将文件的字节传递给 JVM ,通过 JVM ( native 方法)对于 Class 的定义,将其具体化,实例化为一个Class 类型实例。

3,getParent

返回其parent ClassLoader

我们通过实际demo来测试一下。

1
2
3
4
5
6
7
8
9
10
11
package demo;
public class Test {
public static void main(String[] args) {
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
ClassLoader classLoader1 = classLoader.getParent();
System.out.println(classLoader1);
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println(classLoader2);
}
}

打印结果如下:

1
2
3
sun.misc.Launcher$AppClassLoader@135fbaa4
sun.misc.Launcher$ExtClassLoader@2503dbd3
null

在 Java 中提供了以下三种类加载 ClassLoader:

  1. Bootstrp loader
  2. ExtClassLoader
  3. AppClassLoader

其实上面的 demo 打印出来的结果就验证了这三种 ClassLoader。

(1): 根类加载器(null)

它是由本地代码(c/c++)实现的,你根本拿不到他的引用,但是他实际存在,并且加载一些重要的类,它加载(%JAVA_HOME%\jre\lib),如rt.jar(runtime)、i18n.jar等,这些是Java的核心类。

(2): 扩展类加载器(ExtClassLoader)

虽说能拿到,但是我们在实践中很少用到它,它主要加载扩展目录下的jar包, %JAVA_HOME%\lib\ext

(3): 应用类加载器(AppClassLoader)

它主要加载我们应用程序中的类,如Test,或者用到的第三方包,如jdbc驱动包等。

这里的父类加载器与类中继承概念要区分,它们在class定义上是没有父子关系的。

类加载器调用顺序

同样的,在 Java 中的 ClassLoader 存在一种调用的顺序,我们就以上面的 Test.java 类进行解析。

当 Test.class 要进行加载时,它将会启动应用类加载器进行加载Test类,但是这个应用类加载器不会真正去加载他,而是会调用看是否有父加载器,结果有,是扩展类加载器,扩展类加载器也不会直接去加载,它看自己是否有父加载器没,结果它还是有的,是根类加载器。

所以这个时候根类加载器就去加载这个类,可在%JAVA_HOME%\jre\lib 下,它找不到 demo.Test 这个类,所以他告诉他的子类加载器,我找不到,你去加载吧,子类扩展类加载器去 %JAVA_HOME%\lib\ext 去找,也找不着,它告诉它的子类加载器 AppClassLoader,我找不到这个类,你去加载吧,结果AppClassLoader 找到了,就加到内存中,并生成 Class 对象。

借用网上的一张图来表示流程。
在这里插入图片描述

这张图很明显的展示了 ClassLoader 的执行顺序流程。

双亲委托

双亲委托即“类装载器有载入类的需求时,会先请示其 Parent 使用其搜索路径帮忙载入,如果 Parent 找不到,那么才由自己依照自己的搜索路径搜索类”,上面那张图就显示了该流程。

我们还是使用上面的代码进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
package demo;
public class Test {
public static void main(String[] args) {

ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
ClassLoader classLoader1 = classLoader.getParent();
System.out.println(classLoader1);
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println(classLoader2);
}
}

打印结果如下:

1
2
3
sun.misc.Launcher$AppClassLoader@135fbaa4
sun.misc.Launcher$ExtClassLoader@2503dbd3
null

打印结果可以看出,Test是由 AppClassLoader 加载器加载的,AppClassLoader 的 Parent 加载器是 ExtClassLoader ,但是ExtClassLoader 的 Parent 为 null 是怎么回事呵,朋友们留意的话,前面有提到 Bootstrap Loader 是用 C++ 语言写的,依 java 的观点来看,逻辑上并不存在 Bootstrap Loader 的类实体,所以在 java 程序代码里试图打印出其内容时,我们就会看到输出为 null。

为什么要有“委托机制”?可以从安全方面考虑,如果一个人写了一个恶意的基础类(如java.lang.String)并加载到 JVM 将会引起严重的后果,但有了全盘负责制,java.lang.String 永远是由根装载器来装载,避免以上情况发生。

假如我们自己写了一个 java.lang.String 的类,我们是否可以替换调JDK 本身的类?

答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个 classLoader 来加载自己写的java.lang.String 类,但是你会发现也不会加载成功,具体就是因为针对java.* 开头的类,jvm 的实现中已经保证了必须由 bootstrp 来加载。

自定义 ClassLoader

既然系统已经存在三种 ClassLoader 为什么还需要我们自己定义 ClassLoader 呢?因为 Java 中提供的默认 ClassLoader 只加载指定目录下面的 jar 和 class ,但是如果我们需要加载其他地方的 jar 和 class 时则无能为力了,这个时候就需要我们自己实现 ClassLoader 了。我们从上面了解到 ClassLoader是一个抽象类,实现自定义的 ClassLoader 需要继承该类并实现里面的方法。一般情况下,我们重写父类的 findClass 方法即可。

1
2
3
4
5
6
7
8
9
10
package java.lang;
public abstract class ClassLoader {
public Class loadClass(String name);
protected Class defineClass(byte[] b);
public URL getResource(String name);
public Enumeration getResources(String name);
public ClassLoader getParent();
Class<?> findClass(String name)
//...
}

ClassLoader 方法那么多为什么只重写 findClass 方法? 因为 JDK 已经在 loadClass 方法中帮我们实现了 ClassLoader 搜索类的算法,当在 loadClass 方法中搜索不到类时,loadClass 方法就会调用findClass 方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写 loadClass 搜索类的算法。

自定义 ClassLoader的示例

假如我们自定义一个 classloader 我们可以编写一个测试类来说明。在当前目录下面新建一个 Hello 类。里面有个方法 sayHello 然后放入到指定目录下面,如:我当前的目录为:

/Users/java/intellidea/JavaTest/src/demo/Hello.java

1
2
3
4
5
6
package demo;
public class Hello {
public void sayHello() {
System.out.println("say hello ------> classloader");
}
}

接着我们需要自定义一个 ClassLoader 来继承系统的 ClassLoader。我们命名为 HelloClassLoader 类。

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
package demo;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class HelloClassLoader extends ClassLoader {
private String mLibPath;
public HelloClassLoader(String path) {
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getFileName(name);
File file = new File(mLibPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index)+".class";
}
}
}

在HelloClassLoader 类中我们通过 findClass() 方法来查找我们用到的 Hello.class 文件,从而生成了 Class 对象。接着我们编写HelloClassLoaderTest 测试类进行测:

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
package demo;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class HelloClassLoaderTest {
public static void main(String[] args) throws InvocationTargetException {
// TODO Auto-generated method stub
//创建自定义classloader对象。
HelloClassLoader diskLoader = new HelloClassLoader("/Users/java/intellidea/JavaTest/src/demo");
try {
//加载class文件
Class c = diskLoader.loadClass("demo.Hello");
if (c != null) {
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("sayHello", null);
//通过反射调用Hello类的sayHello方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

测试类按照预测结果应该打印:

1
say hello ------> classloader

这行代码是我们在 Hello 类中的一个方法 sayHello 里面所打印的。我们运行一下上面的代码,查看下打印结果:

在这里插入图片描述

如料想的一样,打印出了正确结果。以上便是一个简单的自定义ClassLoader 类的实现过程。

总结

以上便是关于 Java 中的 ClassLoader 里面的内容,还有一些暂未涉及到,接下来便是研究 Android中 的 ClassLoader 使用内容。


欢迎关注微信公众号