Java ClassLoader详解

Contents
  1. 背景
  2. ClassLoader
    1. Java中ClassLoader分类
    2. ClassLoader的加载机制
    3. 自定义ClassLoader
      1. 关键方法
      2. 例子
    4. JVM如何判断两个类是否相同
  3. 补充

源自朋友踩的一个坑:这位仁兄将一个工具类的静态方法加上synchronized关键字后,预期在各个线程之间做同步,预期是在获得类锁后访问该静态方法的各个线程互斥。暂且不谈效率问题,他告诉我这样synchronized之后同步失败了,显然是不科学的。在讨论一番之后,发现他是在两个service中分别启动了一个线程,这两个线程想要互斥的访问加载上来的工具类的静态方法。。。

很明显他没有弄清楚类加载时如何判断两个类是否相同的问题,先给结论:

  • 是否具有相同类名;
  • 是否由同一个ClassLoader加载

这里他通过两个service加载同一个工具类,也就是使用的两个ClassLoader实例,事实证明,打印出来的ClassLoader的hashcode也是不同的,印证了猜想。

顺带整理下ClassLoader的相关问题。

背景

Java程序是由一个个class文件组成的,在程序执行阶段,JVM采用了动态加载的策略,当一个类被使用时,将由一个ClassLoader实例去加载这个class文件。Java允许我们从外部加载一个类到内存中,然后运行它。

ClassLoader

Java中ClassLoader分类

  • BootstrapClassLoader:启动类加载器,它负责在程序启动时去加载Java的核心库。
  • ExtensionClassLoader:扩展类加载器,它负责加载扩展库。
  • AppClassLoader:系统类加载器,它负责加载classpath下的.class文件。
  • CustomClassLoader:自定义的ClassLoader。

前面三种是Java系统自带的ClassLoader,最后一个可以自定义。

ClassLoader的加载机制

ClassLoader采用了双亲委托加载机制

classLoader

  1. 可以看到,ClassLoader在加载一个类时,会先自下而上的检查目标是否被加载了;
  2. 然后,自上而下的依次尝试去加载目标,如果到最后一层ClassLoader仍然没有加载到,就会抛出ClassNotFoundException错误。
  3. 注意图中每种类型ClassLoader负责的范围。

自定义ClassLoader

关键方法

  • findClass(String name):这个方法顾名思义负责查找一个类,并返回它。对我们自定义而言,这是我们最需要关注的,一般情况下,我们只需要直接在这个方法中返回目标类就可以了,这也是Google推荐我们的做法。
  • loadClass(String name):这个方法中主要负责协调加载类,通常它的逻辑比较固定,我们可以不去重写。在这个方法中,先尝试通过父类ClassLoader去加载目标类,没有加载到,然后调用findClass()方法去查找。
  • defineClass(String name, byte[] b, int off, int len):负责定义类,这个方法我们主要调用就好了。

例子

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
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
super(CustomClassLoader.class.getClassLoader());
this.classPath = classPath;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
//检查路径是否可用
if (classPath == null || classPath.equals("")) {
throw new IllegalArgumentException("Please set class path first.");
}
//加载class文件数据
byte[] classData = loadClassData(classPath);
if (classData == null) {
throw new NullPointerException(
"Try to get the byte[] that read from class file, but mate some problem. Please check class file path.");
}
return defineClass(name, classData, 0, classData.length); // 将class的字节数组解码为Class实例
}
/**
* 读取Class文件。这就是一个读文件的操作嘛!
*/
private byte[] loadClassData(String path) {
byte[] bytes = new byte[1024];
int length = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
File classFile = new File(path);
FileInputStream fis = null;
try {
fis = new FileInputStream(classFile);
while ((length = fis.read(bytes)) != -1) {
baos.write(bytes, 0, length);
baos.flush();
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fis == null) {
throw new NullPointerException(
"Can not create FileInputStream, please check the file path.");
}
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public void setClassPath(String classPath) {
this.classPath = classPath;
}
}
//下面来看看怎么使用
public class MyClass {
public static void main(String[] args){
CustomClassLoader classLoader = new CustomClassLoader("");
try {
classLoader.setClassPath("/Users/.../TestClass.class");
Class clazz = classLoader.loadClass("TestClass");
//通过反射来调用他的方法
Method method = ReflectUtils.getMethod(clazz,"doSomething");
System.out.println(clazz.getSimpleName());
System.out.println("result = " + method.invoke(clazz.newInstance()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
//下面是TestClass
public class TestClass {
public int doSomething(){
return 998;
}
}
//下面是输出结果
TestClass
result = 998

JVM如何判断两个类是否相同

  1. 是否具有相同类名;
  2. 是否由同一个ClassLoader加载。

补充

有对ClassLoader源码感兴趣的同学,可以移步这里