Java 利用 ClassLoader 实现类的热加载和卸载
最近做项目的时候遇到这个问题,就是在服务器不重启的情况下实现版本替换,即项目中一部分的类需要实时替换,甚至,对利用动态编译去生成很多临时类;
那就需要解决两个问题:
1. 如何实现类的动态替换
2. 如何实现类的卸载
当然,关于类的动态编译,我会在今后的文章中写出;
一、实现类的动态替换:
首先,同名同包的类,在静态编译的时候是肯定无法通过的,在运行时也一定不行;这是为什么呢?
因为,所有的类,在未实现自定义 ClassLoader 的时候,都会由系统提供的三个 ClassLoader 来加载,分别是:
BootstrapClassLoader:
Java 本地类,在 Java 语言层面是透明的,由 C++ 实现(仅这个),负责加载 JVM 自身需要的类,这些类放在 <JAVA_HOME>/lib 路径下的核心类库或 - Xbootclasspath 参数指定的路径下的 jar 包内,并根据文件名识别 jar 包,如 rt.jar.. 出于安全考虑,BootstrapClassLoader 只加载包名为 java、javax、sun 等开头的类。
ExtensionClassLoader:
它负责加载 <JAVA_HOME>/lib/ext 目录下或者由系统变量 - Djava.ext.dir 指定位路径中的类库,开发者可以直接使用标准扩展类加载器
AppClassLoader:
它负责加载系统类路径 java -classpath 或 - D java.class.path 指定路径下的类库,也就是我们经常用到的 classpath 路径
类的加载采用按需加载和双亲委托的机制,什么意思呢?
当一个类需要被加载时,JVM 才会将.class 读取进内存,进行之后的加载操作;
双亲委托是指,当前的 classloader 会找自身这个类是否被加载,找到就返回;如果没找到,会向它的父加载器查询是否被加载,直到找到 BootstrapClassLoader 仍未加载时,向下返回,返回到初始调用的 classloader,加载该类;
理解了这个机制,当我们需要动态加载一个类时,当前的 classloader 会去寻找该类是否被加载,由于在服务器运行过程中,需要替换的类已经被加载,从双亲委托的角度看,这个类已经被加载,在加载就会报错。
有没有办法绕过双亲委托机制呢?
简单啊!
我们自定义一个类加载器,同时不指定它的父加载器,一个 classloader 只负责加载一个类,那么当新的类需要加载时,我们为其生成一个新的 classloader 即可;
可能细心的读者会问了,那新的类是加载进去了,旧的类怎么办啊,如果我不断有新的类加载,不一样会导致内存占用吗?
这就涉及类的卸载了:
二、类的卸载
在 Java 中,每个类都有相应的 classloader,同样的,每个实例对象也会有相应的类,当满足如下三个条件是,gc 就会卸载这个类:
1. 该类所有实例对象不可达
2. 该类的 Class 对象不可达
3. 该类的 ClassLoader 不可达
下面我将详细解答:
由于每个对象都有相应的 Class 对象,所以当该类仍有实例的时候,是无法卸载的,因为此时 Class 对象仍可达;
对于 ClassLoader 对象,留意双亲委托机制中,每个 ClassLoader 都会记录自身已加载的类信息,所以如果 ClassLoader 可达,那么 Class 对象仍是可达的,这就解释了为什么我们为什么需要自定义 ClassLoader,因为系统的 ClassLoader 永远是可达的,他们加载的类在运行时永远不会被卸载;
那现在问题就简单了,我们在加载类的方法时,定义一个临时的 ClassLoader,返回结果为 Class 对象,当这个方法结束后,就仅有该 Class 对象可以获取到这个 ClassLoader;也就是说,当该类的所有实例对象都被 gc 后,就仅有 Class 对象可以获得这个 ClassLoader 了,当我们把这个 Class 置为空并进行 gc 后,这个类就会被卸载;
三。类的监控
说了这么多,我们有没有什么方法能够监控 JVM 的加载和卸载过程呢?
其实很简单,只需在 JVM 的启动参数中加上几行配置即可:
-verbose:class 用于同时跟踪类的加载和卸载
-XX:+TraceClassLoading 单独跟踪类的加载
-XX:+TraceClassUnloading 单独跟踪类的卸载
代码如下:使用 JVM 参数:-verbose:class
// 死循环模拟生产环境
while (true){
// 定义一个匿名类加载器,指定类的父加载器为空,绕过双亲委托机制
ClassLoader loader = new ClassLoader(null) {
// 重写 findClass 方法,加载指定文件,这个部分你可以自由发挥
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File classFile = new File("d://Sample.class");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
InputStream stream = new FileInputStream(classFile);
int b = 0;
while((b = stream.read())!=-1){
outputStream.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] bytes = outputStream.toByteArray();
return super.defineClass("com.sun.InterfaceFactory.Sample", bytes, 0, bytes.length);
}
};
// 加载需要的类
Class c = loader.loadClass("");
-
c = null;
-
System.out.println("gc1");
-
System.gc();
-
try {
-
Thread.sleep(3000);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
loader = null;
-
System.out.println("gc2");
-
System.gc();
-
try {
-
Thread.sleep(3000);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
输出结果:
[Loaded com.sun.InterfaceFactory.Sample from JVM_DefineClass]
gc1
gc2
[Unloading class com.sun.InterfaceFactory.Sample 0x00000007c0094828]
[Loaded com.sun.InterfaceFactory.Sample from JVM_DefineClass]
gc1
gc2
[Unloading class com.sun.InterfaceFactory.Sample 0x00000007c0094828]
.....
参考:
https://blog.csdn.net/chaofanwei2/article/details/51298818
1、classloader 介绍
热部署,即需要 jvm 释放之前加载的业务 class,且重新加载最新的业务 class,并释放之前的 class(卸载),其实类和普通对象一样都是对象,即如果从 gc root 除非,没有引用此类的别的对象存在,即会被 jvm 自动回收。
class 文件在加载时,会把二进制文件放在内存中,并会在堆取 new 出一个表示此 class 的 class 对象,然后若 new 类对象,则再把此 new 出来的对象的 class 对象指向刚才创建的 class 对象.

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
类加载卸载图:

2、jdk 自带 classload 介绍
还是直接看图吧
2.1 类图

2.2 Classloader.loadClass (String name,boolean resolve) 流程图

2.3 URLClassLoader.findClass (String name) 流程图

Java 是跨平台的,指得是 Java 编译成的 class 文件,可以在 (32/64) .* (Oracle/IBM/…) .* (Linux/Win) 等不同的 JVM 实现下面运行,Java 源文件在不同的 JVM 下面不需要重新编译。但在不同的 OS 下面可能需要安装不同的 JVM (这个程度上说,某个 JVM 实现不是跨平台的)。
类加载过程
首先要理解什么是 jvm,jre,jdk,这里就不再详细叙述,简单的说 jdk 包含 jre,jre 包含 jvm。jvm 只是执行编译后的 class 文件的,class 文件中存储的是操作指令,而 jvm 则是一个指令执行引擎。
java 类加载过程是有 jvm 控制的,用户代码不可以控制。具体过程分为 1 加载 2 验证 3 准备 4 连接 5 初始化 6 使用 7 卸载。
在加载过程中主要是根据一个类的全限定名来加载该类的二进制流到内存中,并形成该类的内存结构(方法区中),最后并形成代表该类的 class 文件(方法区中)。
验证过程相对简单,主要是由 jvm 根据指定的规则来验证该 class 文件是否合法,是否有错误,比如在网络传输的过程中不完整等等。
准备阶段则是正式为该类的静态字段分配内存并赋初值,通常是 0 值和 false。
连接则是把 class 文件中的符合引用转换为直接引用的过程,在 class 文件中存储的是符号引用。
初始化则是根据用户的设定正式为静态字段赋值,按照类中的顺序,包括 static 代码块。不同的顺序,结果可能不同。
卸载则是把该类从内存中卸载掉。
类加载器
在整个类加载过程中,只用类的加载可以有用户控制,而在类加载阶段则又出现了一些很实用的技术,类加载器。
在 java 中类加载器使用双亲委派模型来加载类,bootstrap classloader 是有 c++ 写的,是根加载器。然后下面是 ext classloader 和 AppClassLoader,所谓双亲委派加载是指当一个类加载器在加载一个类时,首先由该类加载器的父加载器加载,如果父加载器不能加载的话,才有该类加载器加载类。这里的父并不是 extend 而是 composite。看 java ClassLoader 的源码 ClassLoader 是抽象类,是所有类加载器的父类。classload 类 源码 http://blog.csdn.net/chaofanwei/article/details/12858833
URLClassLoader 类源码 http://blog.csdn.net/chaofanwei/article/details/12858971
类结构图:


类加载路径:
BootstrapLoader : sun.boot.class.path
ExtClassLoader: java.ext.dirs
AppClassLoader: java.class.path
这三个系统参量可以通过 System.getProperty() 函数得到具体对应的路径。
sun.boot.class.path 一般是 java_home/lib
java.ext.dirs 一般是 java_home/lib/ext
每个类加载器都有一个名称空间,子类加载器可以看到父类加载器空间内的类,而反过来不可以。
每个类的唯一性是有该类的类加载器和类共同决定的。
线程上下文类加载器,是一个比较特殊的类加载器,可以被根类加载器调用用来加载位于 classpath 中的类。

浙公网安备 33010602011771号