0%

了解ClassLoader

初识ClassLoader

  1. 在开发中有时会碰到ClassNotFoundException。这个异常和ClassLoader有着密切的关系。

  2. 我们常使用instanceof关键字判断某个对象是否属于指定Class创建的对象实例。如果对象和Class不属同一个加载器加载,那么instanceof返回的结果一定是false。

  3. GC Root有一种叫做System Class,官方解释“Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util. .”,大意是:被bootstrap/system加载器加载的类,比如,像java.util.这些来自rt.jar的类。

  4. GC时对Class的卸载,需要满足的条件如下:

类需要满足以下3个条件才能算是“无用的类”

  • 该类所有的实例已经被回收
  • 加载该类的ClassLoder已经被回收
  • 该类对应的java.lang.Class对象没有任何对方被引用

    ClassLoader简介

我们的Java应用程序都是由一系列编译为class文件组成,JVM在运行的时候会根据需要(比如:我们需要创建一个新对象,但是该对象的Class定义并未在Perm区找到)将应用需要的class文件找到并加载到内存的指定区域供应用使用,完成class文件加载的任务就是由ClassLoader完成。

ClassLoader类的基本职责就是根据一个指定的类的名称,找到(class可能来源自本地或者网络)或者生成其对应的字节代码,然后从这些字节代码中定义出一个java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

在JVM中每个ClassLoader有各自的命名空间,不同的ClassLoader加载的相同class文件创建的Class实例被认为是不相等的,由不相等的Class创建的对象实例无法相互强制转型,如开头所提,当我们使用instanceof关键字判断时需要注意。

双亲委派模型

双亲委派模型很好理解,直接上代码。

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
/**
* 使用指定的二进制名称来加载类。默认的查找类的顺序如下:
* 调用findLoadedClass(String) 检查这个类是否被加载过;
* 调用父加载器的loadClass(String),如果父加载器为null,使用虚拟机内置的加载器代替;
* 如果父类未找到,调用findClass(String)方法查找类。
*/
// ClassLoader
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 {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.

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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

从源码中我们看到有三种类加载器:

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码(C++)来实现的,并不继承自java.lang.ClassLoader。负责将${JAVA_HOME}/lib目录下和-Xbootclasspath参数所指定的路径中的,并且是Java虚拟机识别的(仅按照文件名识别,如rt.jar,不符合的类库即使放在lib下也不会被加载)类库加载到JVM内存中,引导类加载器无法被Java程序直接引用;

  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库(${JAVA_HOME}/ext),或者被java.ext.dirs系统变量所指定的路径中的所有类库;

  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载Java类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ClassLoaderTree {
/**
* 输出:
* sun.misc.Launcher$AppClassLoader@18b4aac2
* sun.misc.Launcher$ExtClassLoader@5305068a
* null
* @param args
*/
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader!=null){
System.out.println(loader.toString());
loader = loader.getParent();
}
System.out.println(loader);
}
}

每个Java类都维护着一个指向定义它的类加载器的引用,通过getClassLoader()方法就可以获取到此引用。通过调用getParent()方法可以得到加载器的父类,上述代码输出中,AppClassLoader对应系统类加载器(system class loader);ExtClassLoader对应扩展类加载器(extensions class loader);需要注意的是这里并没有输出引导类加载器,这是因为有些JDK的实现对于父类加载器是引导类加载器。这些加载器的父子关系通过组合实现。

为什么要双亲委派

  1. 避免重复加载。当父亲已经加载了该类,子类就没有必要再加载一次;
  2. 安全。如果不使用这种委托模式,那我们就可以使用自定义的String或者其他JDK中的类,存在非常大的安全隐患,而双亲委派使得自定义的ClassLoader永远也无法加载一个自己写的String。

创建自定义ClassLoader

自定义的类加载器只需要重写findClass(String name)方法即可。java.lang.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
32
33
34
35
36
37
38
39
40
41
42
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;

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

@Override
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 = 1024;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1){
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

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

实验

更多实验代码放在github上:
https://github.com/Childe-Chen/goodGoodStudy/tree/master/src/main/java/com/cxd/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
public class TestClassIdentity {

public static void main(String[] args) {
FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader("/Users/childe/Documents/workspace/goodGoodStudy/target/classes");

try {
Class<?> c = fileSystemClassLoader.findClass("com.cxd.classLoader.Sample");

//forName会执行类中的static块(初始化)
Class<?> c1 = Class.forName("com.cxd.classLoader.Sample");
System.out.println(c1.isAssignableFrom(c));

//运行时抛出了 java.lang.ClassCastException异常。虽然两个对象 o1 o2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。
//不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。
// 不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到
Object o = c.newInstance();
Method method = c.getMethod("setSample", java.lang.Object.class);

Object o1 = c1.newInstance();
method.invoke(o,o1);

} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Sample {
private Sample instance;

static {
System.out.println("static");
}

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

打破双亲委派模型

没有完美的模型,双亲委派在面对SPI时,不得不做出了特例或者说改进。我们知道Java提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的有JDBC、JCE、JNDI、JAXP 和 JBI 等。SPI的接口由Java核心库定义,而其实现往往是作为Java应用所依赖的 jar包被包含到CLASSPATH里。而SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,由引导类加载器加载;SPI的实现类是由系统类加载器加载,引导类加载器无法找到SPI的实现类,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

为了解决这个问题,Java引入了线程上下文类加载器,在Thread中聚合了contextClassLoader,通过Thread.currentThread().getContextClassLoader()获得。原始线程的上下文ClassLoader通常设定为用于加载应用程序的类加载器。也就是说父加载器可以通过线程上下文类加载器可以获得第三方对SPI的实现类。

以Java链接Mysql为例,看下Java如何来加载SPI实现。

1
2
3
4
5
// 注册驱动,forName方法会初始化Driver,初始化块中向DriverManager注册驱动
Class.forName("com.mysql.jdbc.Driver").getInstance();
String url = "jdbc:mysql://host:port/db";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");

com.mysql.jdbc.Driver是java.sql.Driver的一种实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
// 向DriverManager注册驱动
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
...
}

接下来我们调用getConnection就进入了本小结的关键点。

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
//  Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* 再次强调下:原始线程的上下文ClassLoader通常设定为用于加载应用程序的类加载器
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
//caller由Reflection.getCallerClass()得到,而调用方是java.sql.DriverManager,所以getClassLoader()是引导类加载器,也就是null
//所以此处使用线程上下文加载器来加载实现类
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;

for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
// isDriverAllowed中使用给定的加载器加载指定的驱动
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}

// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

总结&扩展

  • 双亲委派作为基本模型,隔离了不同的调用者,保证了程序的安全。
  • 线程上线文加载器与其说破坏了双亲委派倒不如说是扩展了双亲委派的能力,使其有更好的通用性。
  • Tomcat、Jetty等Web容器都是基于双亲委派模型来做资源的隔离。
  • Spring在设计中也考虑到了类加载的问题,详细可见:
    org.springframework.web.context.ContextLoader.initWebApplicationContext(…)。

小生不才,以上如有描述有误的地方还望各位不吝赐教 !^_^!

参考

http://www.infocool.net/kb/Tomcat/201609/193323.html
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://github.thinkingbar.com/classloader/