类加载器classloader-java简析

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

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

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

1,Class 再认识

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

1
javac HelloWorld.java

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

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

1
java HelloWorld

结果输出 “Hello Java World !”

以上便是一个完整的 Java 文件运行过程,先经过编译将 .java 文件转为 .class 格式以便 JVM能够执行。接下来我们来详细分析。

2, Java 类加载器

2.1,ClassLoader 分类

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

1
2
3
4
5
6
7
8
9
10
11
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
12
13
14
15
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定义上是没有父子关系的。

2.2, 类加载器调用顺序

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

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

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

借用网上的一张图来表示流程。

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

2.3, 双亲委托

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 来加载。

3,自定义 ClassLoader

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

1
2
3
4
5
6
7
8
9
10
11
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 搜索类的算法。

3.1,自定义 ClassLoader的示例

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

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

1
2
3
4
5
6
7
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
33
34
35
36
37
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 类的实现过程。

4,总结

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


参考链接

1、http://blog.csdn.net/briblue/article/details/54973413
2、https://www.cnblogs.com/wang-meng/p/5574071.html
3、https://www.cnblogs.com/doit8791/p/5820037.html


关于作者:

1. 博客 http://crazyandcoder.github.io/

2. github https://github.com/crazyandcoder

3. 掘金 https://juejin.im/user/56b96af96240b8005865df59/share