0%

java类加载器问题思考与简单模拟热部署

java中,ClassLoader负责读取class文件、加载到JVM

类加载过程

类加载包含的过程:

  • 加载,根据类的完全限定查找字节码文件并加载、创建一个Class对象
  • 验证,验证class文件符合JVM要求
  • 准备,为static变量分配内存并初始化为初始值(不管是否显式指定了初始化,都先初始化为变量类型默认的初始值)
  • 解析,为常量值创建直接引用
  • 初始化,类的初始化(static变量初始化、static代码块执行)

所以说,在类加载时就初始化了static量和常量,static量和常量是和class对象一样存储在方法区的。此处要说明的是,比如A中引用了B类作为静态变量,那么在加载A类时,并不会加载B,而是作为一个引用直接默认初始化为null,在初始化A时,才会加载static变量引用的类(静态变量都是如此,也不必说成员变量了)(https://stackoverflow.com/questions/48621815/static-fields-in-jvm-loading)

另外,一个类在只引用了static变量时,会先完成static变量显式初始化和static代码块,而不会初始化成员变量、执行构造器。考虑全部的话,顺序是:父类 static 变量 –> 父类 static 代码块 –> 子类 static 变量 –> 子类 static 代码块 –> 父类显式初始化 –> 父类构造代码块 –> 父类构造器 –> 子类显式初始化 –> 子类构造代码块 –> 子类构造器

初始化的顺序就是,保证在变量可以被访问时,一定已经初始化了。所以,先static再成员变量,先初始化代码块再构造器,先父类再子类;在子类执行成员变量初始化、构造器之前,因为可能引用到父类的成员变量,所以一定先完成父类的成员变量显式初始化、构造代码块、构造器

另外一个情况明确下:比如,父类static量引用了子类static量,那么从子类static得到的是类型默认的初始值,而不会先初始化子类static量。但是这种情况可以认为不存在(子类肯定知道父类的存在,但是父类不应该知道子类的存在;这种情况并发时可能在类加载时导致死锁)

各种ClassLoader

  • Bootstrap类加载器,C++实现,加载/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,主要是java.xxx等核心类
  • Extension类加载器,java实现,加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,java代码中可以使用
  • System类加载器/APP类加载器,加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,开发者引入的依赖和编写的类

关于父类和父加载器

APP类加载器的父加载器是Extension类加载器,Extension类加载器的父加载器是null,或者可以认为是Bootstrap类加载器(C++实现)

自定义的类加载器的父加载器不指定时默认都是APP类加载器

父加载器和父类、子类没有关系。从类继承关系上来说,Bootstrap类加载器是C++实现的,不考虑;Extension类加载器和APP类加载器都是继承自URLClassLoader,再向上都继承自抽象类ClassLoader

一些特点

延迟加载

在使用到时才加载,比如:

  • 显式加载,比如Class.forName()(典型的,加载数据库jdbc驱动)、getClass().getClassLoader().loadClass()
  • 隐式加载,使用到时自动加载

关于Class.forName(),要注意的是,这一方法默认会将类加载并初始化(初始化static量和static代码块)。所以加载jdbc驱动时,只需要Class.forName(“com.mysql.jdbc.Driver”),因为Driver类使用了static代码块初始化

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
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// ~ Static fields/initializers
// ---------------------------------------------

//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

// ~ Constructors
// -----------------------------------------------------------

/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

而loadClass()只加载、默认不解析(相关的是resolveClass()方法)、不初始化

双亲委派

在loadClass()方法中加载类,默认的实现中流程都是:

  • findLoadedClass()从缓存检查是否已经加载,已经加载时直接返回
  • 交由父加载器加载,父加载器继续交由父加载器,即递归的向上请求,父加载器为null时请求Bootstrap加载器加载(所以说可以认为Bootstrap加载器是Ext加载器的父加载器)
  • 如果父加载器不能加载,那么自行调用findClass()方法找到类并加载

ClassLoader类中实现的findClass()方法没有具体操作,而是直接抛出了一个异常,URLClassLoader类中做出了实现

双亲委派可以保证安全性,核心类库只会由内置的类加载器加载(而且,即使自定义了一个类取包名为java.lang之类,是会报错禁止加载的)

classLoader与class对象

不考虑父加载器,在一个classLoader中,class对象是唯一的,但是在多个不同的classLoader之间,可能某个类使用不同的多个加载器加载了多个class对象,即不同的classLoader加载的实例对象都拥有不同的独立的类名称空间

使用默认的loadClass()方法中,先使用findLoadedClass()方法根据完整类名检查了类是否已经加载。因此,一个classLoader实例,只能加载一个类一次(即使重写了loadClass()方法去掉了findLoadedClass()检查,在重复加载时会报错duplicate class definition)

如果要加载一个class多次,需要每次使用一个新的自定义classLoader实例,并且这个class应该是这一个classLoader自行加载的(而不是委托给父加载器的)。对于一个代码中直接使用了的类,APP类加载器默认已经加载了,即使通过自定义classLoader加载多个class,直接调用构造器new得到的这一个类的实例,关联的仍然是APP类加载器加载的class对象(所以如果将自定义类加载器加载的类的实例cast为代码中的A,是会异常的,关联的不是同一个class对象当然不能cast)

自定义classLoader

可用于实现:

  • 从一个自定义的路径加载类,或者从网络加载类
  • 对于特定路径/网络中读取的字节码加密的class文件,自定义classLoader来实现解密后加载
  • 热部署。假设要从一个特定的目录读取class文件并替换当前实例,可以使用多个classLoader,直接调用findClass()方法从特定目录加载某个class文件,来得到多个class对象;或者指定自定义类加载器的父加载器是null或Ext类加载器,并重写findClass()方法,调用loadClass()加载(下文有说明和实现)

自定义时,文档中不建议重写loadClass()方法,因为可能打破双亲委派模型,建议实现findClass()方法(默认流程中,先尝试父加载器加载,父加载器不能加载时findClass()找到类来加载;如果直接重写loadClass()方法放弃了使用父加载器,对于引用到的java.xxx等类(即使什么也没引用那么也默认extends了Object),自定义类加载器是禁止加载这些核心类的)

findClass()方法用于读取字节码的流,defineClass()用于将字节码的构建为class对象

自定义时,如果继承ClassLoader类,需要重写findClass()方法,在其中读取字节码,然后调用defineClass()方法构建class对象并返回;如果继承URLClassLoader类,那么已经实现了很完备的findClass()方法,可以识别传入的是一个文件路径还是URL等,一般比较简洁

还需要注意的是,一个classLoader加载类A,那么它也需要负责加载A中使用的其他类,实现中可以是委托到双亲或者自行加载

简单模拟热部署

模拟热部署,假设要从一个特定的目录读取class文件并替换当前实例,可以使用多个classLoader,直接调用findClass()方法从特定目录加载某个class文件,来得到多个class对象;或者指定自定义类加载器的父加载器是null或Ext类加载器,并重写findClass()方法,调用loadClass()加载

直接调用findClass()方法显然可以得到这个类的多个class对象

指定自定义类加载器的父加载器是null或Ext类加载器是因为,自定义classLoader默认的父加载器是APP类加载器,APP类加载器是可以加载源代码的类的。既然热部署那么当然这个类在现有代码中已经使用了,APP类加载器已经加载过了,双亲委派模型下无法加载这一个类的多个class对象。而null或Ext类加载器是无法加载源代码中的类的,那么它们将交由自定义类加载器使用findClass()加载

指定自定义类加载器的父加载器为null或者Ext类加载器带来的问题是,比如指定为Ext类加载器,那么自定义类加载器中只能委托到Ext类加载器和Bootstrap类加载器。如果加载的类A之中引用了一个源代码中其他类B,Ext和Bootstrap类加载器都无法加载B,需要自定义类加载器来处理,这种情况中,可以得到A的class对象,在初始化A时,才会加载B,此时如果自定义类加载器不能加载B,那么就会抛出异常

对这个问题,我们可以在自定义类加载器中维护一个负责加载的类的集合,集合之外的类调用APP类加载器进行加载

还存在的一个问题是,代码中直接使用的类A,是由APP类加载器加载的,那么再由自定义类加载器加载一个A时,Class.newInstance()得到的实例,不能cast为代码中的A,因为对应的不是一个class对象,这样使用自定义加载器加载的A时需要通过反射的方式。这个问题可以这样解决:让A实现一个接口C,在使用自定义类加载器加载A时,将接口类交给APP类加载器加载(实际上APP类加载器也应该早已加载过了),那么就可以将A cast为C来使用

代码如下

MyService是要热部署的类,实现了MyInterface接口:

1
2
3
4
5
package classLoaderTest;
public interface MyInterface {

void doSomething(String s);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package classLoaderTest;

public class MyService implements MyInterface {

// MyTest是源代码中其他类,使用自定义类加载器加载MyService时,loadClass()可以得到class对象,初始化时需要自定义类加载器将MyTest类也加载,自定义类加载器实现中调用了APP类加载器来加载MyTest
// 实现的接口MyInterface类的加载也是这样处理的
public static MyTest myTest = new MyTest();
// Integer,只能由BootStrap类加载器加载,自定义实现中没有打破双亲委托模型,所以可以正常加载
// Object等间接使用到的类也是委托双亲的
public Integer num = new Integer(1);

@Override
public void doSomething(String s) {
System.out.println("classLoaderTest.MyService: doSomething: " + s);
}
}

MyClassLoader继承自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
43
44
45
46
47
48
package classLoaderTest;

import java.io.*;
import java.util.HashSet;
import java.util.Set;

public class MyClassLoader extends ClassLoader {

// 读取class文件的路径
// 测试时已经在这一路径存储好了要加载的class文件
private static String classPath = "C:\\Users\\matian\\Desktop";

// set中的类由自定义类加载器加载,其他类将调用APP类加载器加载
private static Set<String> canLoadClassesNames;

public MyClassLoader(ClassLoader parent) {
super(parent);
// 模拟中,自定义类加载器只负责加载MyService类
canLoadClassesNames = new HashSet<>();
canLoadClassesNames.add(MyService.class.getName());
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 集合之外的类,调用APP类加载器加载
if (!canLoadClassesNames.contains(name)) {
return getSystemClassLoader().loadClass(name);
}

// 负责加载的类,读取class文件,调用defineClass()方法构建class对象
try {
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(classPath + File.separator + name + ".class"));
byte[] buf = new byte[1024];
int len = -1;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((len = inputStream.read(buf)) != -1) {
byteArrayOutputStream.write(buf, 0, len);
}
return defineClass(name, byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

throw new ClassNotFoundException();
}
}

以及一个模拟热部署的测试类:

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
public class HotDeployTest {

private static MyInterface myService;

public static void main(String[] args) {
// 代码中直接使用的MyService对应的类对象是由APP类加载器加载的
myService = new MyService();
myService.doSomething("before hot deploy");
System.out.println(MyService.class.getClassLoader().toString());
System.out.println(MyInterface.class.getClassLoader().toString());
System.out.println(myService.getClass().getClassLoader().toString());

// 等待3s后从指定路径加载类,并将myService替换为动态加载的实现
try {
Thread.sleep(3000);

Class cls = new MyClassLoader(null).loadClass("classLoaderTest.MyService");
myService = (MyInterface) cls.newInstance();
System.out.println(MyService.class.getClassLoader().toString());
System.out.println(MyInterface.class.getClassLoader().toString());
System.out.println(myService.getClass().getClassLoader().toString());
System.out.println(cls.getClassLoader().toString());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}

myService.doSomething("after hot deploy");

}

}

以上代码输出:

1
2
3
4
5
6
7
8
9
classLoaderTest.MyService: doSomething: before hot deploy
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
classLoaderTest.MyClassLoader@4554617c
classLoaderTest.MyClassLoader@4554617c
classLoaderTest.MyService: doSomething: after hot deploy