热加载 相关知识,欢迎交流,指正错误。
1. 什么是热加载
热加载 是指可以在不重启服务的情况下让更改的代码生效,热加载 可以显著的提升开发以及调试的效率,它是基于 Java 的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境。
2. 热加载与热部署的区别
首先,不管是热加载 还是热部署 ,都可以在不重启服务的情况下编译/部署项目,都是基于 Java 的类加载器实现的。
两者之间的区别:
在部署方式上:
热部署是在服务器运行时重新部署 项目。
热加载是在运行时重新加载 class文件 。
在实现原理上:
热部署是直接重新加载整个应用 ,耗时相对较高。
热加载是在运行时重新加载 class文件 ,后台会启动一个线程不断检测你的类是否改变。
在使用场景上:
热部署更多的是在生产环境 使用。
热加载则更多的是在开发环境 上使用。线上由于安全性问题不会使用,难以监控。
3. 类加载五个阶段
加载阶段:找到类的静态存储结构,加载到虚拟机,定义数据结构。用户可以自定义类加载器。
验证阶段:确保字节码是安全的,确保不会对虚拟机的安全造成危害。
准备阶段:确定内存布局,确定内存遍历,赋初始值 (注意:是初始值,也有特殊情况)。
解析阶段: 将符号变成直接引用。
初始化阶段:调用程序自定义的代码。规定有且仅有5种情况必须进行初始化。
> 1. new(实例化对象)、getstatic(获取类变量的值,被final修饰的除外,他的值在编译器时放到了常量池)、putstatic(给类变量赋值)、invokestatic(调用静态方法) 时会初始化
2. 调用子类的时候,发现父类还没有初始化,则父类需要立即初始化。
3. 虚拟机启动,用户要执行的主类,主类需要立即初始化,如 main 方法。
4. 使用 java.lang.reflect包的方法对类进行反射调用方法 是会初始化。
5. 当使用JDK 1.7的动态语言支持时, 如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、 REF_putStatic、 REF_invokeStatic的方法句柄, 并且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化。
要说明的是,类加载的 5 个阶段中,只有加载阶段 是用户可以自定义处理的,而验证阶段、准备阶段、解析阶段、初始化阶段都是用 JVM 来处理的。
4. 实现类的热加载
4.1. 基本思路
由类加载的五个阶段可知,只有在加载阶段 用户才可以自定义处理,因此如果由文件监视器实时监测class文件,若如class文件发生改变则将class文件重新加载到虚拟机,就可以简单实现类的热加载。
基本步骤:
自定义类加载器
指定需要热加载的类
利用文件监视器实时监测class文件
class发生改变,重新加载
4.2. 自定义类加载器
设计 Java 虚拟机的团队把类的加载阶段放到的 JVM 的外部实现( 通过一个类的全限定名来获取描述此类的二进制字节流 )。这样就可以让程序自己决定如果获取到类信息。而实现这个加载动作的代码模块,我们就称之为 “类加载器”。
在 Java 中,类加载器也就是 java.lang.ClassLoader. 所以如果我们想要自己实现一个类加载器,就需要继承 ClassLoader 。
接下来看看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
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
public abstract class ClassLoader {
protected Class <?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException {
synchronized ( getClassLoadingLock ( name )) {
// 首先,监测是否已经加载
Class <?> c = findLoadedClass ( name );
if ( c == null ) {
long t0 = System . nanoTime ();
try {
if ( parent != null ) {
//父加载器不为null的话调用父加载器的loadClass
c = parent . loadClass ( name , false );
} else {
//父加载器为null 则调用 Bootstrap ClassLoader
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 ();
//父加载器没有找到,则调用findclass
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 ;
}
}
protected final Class <?> findLoadedClass ( String name ) {
if ( ! checkName ( name ))
return null ;
// 检测这个class是不是已经加载过了
return findLoadedClass0 ( name );
}
// 它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常
protected final Class <?> defineClass ( byte [] b , int off , int len ) throws ClassFormatError {
return defineClass ( null , b , off , len , null );
}
}
由ClassLoader#defineClass可知,class文件可通过二进制形式转换为Class对象,并且在会在当前的ClassLoader中缓存该class已被加载,即通过ClassLoader#findLoadedClass方法检测出来。
自定义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
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
/**
* @author harrison
* 自定义ClassLoader
* 作用:将自己指定目录下 更新的class文件 动态加载到JVM中
*/
public class MyClassLoader extends ClassLoader {
/**
* 项目根目录
*/
private final String rootPath ;
/**
* 需要热加载的 class 记录, 因为有些类是不需要我们加载的 比如 String
*/
private final List < String > clazzList ;
/**
* 传入指定目录 热加载class文件
* @param rootPath 项目的根目录,需要根据此目录来截取class文件路径 因为加载的是class文件, 其目录为: /projects/com/xx/X.class
* 但是classLoader需要根据ClassName加载,而className的格式为 com.xx.X,所以需要根据路径截取到com这一截
* @param clazzPaths 需要热加载的类路径
*/
public MyClassLoader ( String rootPath , String ... clazzPaths ) {
this . rootPath = rootPath ;
this . clazzList = new ArrayList <> ();
for ( String clazzPath : clazzPaths ){
loadClassPath ( new File ( clazzPath ));
}
}
/**
* 根据目录扫描项目里的class文件 并把文件加载进JVM
* defineClass() 此方法为ClassLoader的方法
* 此方法传入一个className 与 byte数组(byte数组是对应Class文件的二进制数据数组) 来将对应的Class文件加载进JVM, 并生成Class对象
* @param file 出入扫描的目录
*/
private void loadClassPath ( File file ) {
if ( file . isDirectory ()){
for ( File f : Objects . requireNonNull ( file . listFiles ())){
loadClassPath ( f );
}
} else {
String fileName = file . getName ();
String filePath = file . getPath ();
String endName = fileName . substring ( fileName . lastIndexOf ( Constants . DOT_STRING ) + 1 );
if ( ! CLASS_SUFFIX . equals ( endName )){
return ;
}
try ( InputStream in = new FileInputStream ( file )) {
byte [] bytes = new byte [ ( int ) file . length () ] ;
in . read ( bytes );
String className = filePath2ClassName ( filePath );
clazzList . add ( className );
// 将class文件生成class对象
defineClass ( className , bytes , 0 , bytes . length );
LogUtil . logger . info ( "{} 已加载进JVM" , className );
} catch ( Exception e ) {
LogUtil . logger . error ( "error. " , e );
}
}
}
/**
* 将文件路径替换为ClassName
* @param filePath 文件路径
* @return fileName
*/
private String filePath2ClassName ( String filePath ) {
String className = filePath . replace ( rootPath , Constants . EMPTY_STRING )
. replace ( Constants . CUR_SYSTEM_FILE_SEPARATOR , Constants . DOT_STRING );
className = className . substring ( 0 , className . lastIndexOf ( Constants . DOT_STRING ));
className = className . substring ( 1 );
return className ;
}
}
ClassLoader类中可以看到类加载采用“双亲委派机制”,并且自定义的MyClassLoader的父加载器默认为AppClassLoader,因此如果是这样的话,依旧采用的“双亲委派机制”来加载类,无法达到热加载的效果。故我们需要指定自定义的MyClassLoader来加载。这里涉及到全盘委托 。
全盘委托:首先要用哪个类加载器。
是利用当前方法(或者说当前类)的类加载器作为优先的类加载器。
例如:User对象调用setAddress()方法,而在setAddress()方法中对Address类进行实例化( new Address() ),则Address将优先选择 加载User的类加载器来加载Address,即如果User是AppClassLoader加载,则依据全盘委托 ,Address也是优先选择AppClassLoader进行加载,但最终由哪个类加载器加载,还是由双亲委派机制 决定。假设在User对象中实例化了一个String对象,则会优先选择AppClassLoader加载String,但是由于双亲委派机制最终String由Bootstrap ClassLoader加载。
双亲委派机制:最终用哪个类加载器来加载类。
因此依据全盘委托 我们自己自定义的优先选择类加载器,但是最终选择哪个类加载器还是不确定的。基于此我们可以让自定义的CLassLoader在加载class不经历双亲委派 ,那不就是优先选择谁,谁就是最终的类加载器么。
因此在加载给定目录下的类时,我们先把class文件读取成二进制数组,然后调用defineClass()就可以把class加载进JVM,且不经历双亲委派(如MyClassLoader#LoadClassPath方法所示)。
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
public class Application extends FileAlterationListenerAdaptor {
/**
* 需要热加载类的根目录
*/
public static String rootPath ;
public void start () {
//类似于SpringBoot启动流程。。。
init ();
// logic code
new User (). sayHello ();
}
public void init (){
LogUtil . logger . info ( "初始化项目。" );
}
public static void run ( Class <?> clazz ) throws Exception {
String rootPath = clazz . getResource ( "/" ). getPath (). replace ( "%20" , " " );
rootPath = new File ( rootPath ). getPath ();
Application . rootPath = rootPath ;
startFileMoni ( rootPath );
MyClassLoader myClassLoader = new MyClassLoader ( rootPath , rootPath + Constants . PACKAGE_PATH );
start0 ( myClassLoader );
}
/**
* Application 由自定义的类加载加载,因此在Application中实例化的对象都会优先选择自定义的类加载器
* @param myClassLoader 自定义类加载器
* @throws Exception ex
*/
public static void start0 ( MyClassLoader myClassLoader ) throws Exception {
// loadClass(String name) 参数name是class的全限定名
Class <?> clazz = myClassLoader . loadClass ( "cn.harrison.Application" );
Object o = clazz . newInstance ();
clazz . getMethod ( "start" ). invoke ( o );
}
public static void close (){
LogUtil . logger . info ( "关闭项目" );
//通知JVM销毁已失去引用的对象(执行finalize()方法)
System . runFinalization ();
//通知JVM GC
System . gc ();
}
/**
* 启动文件监听器
* @param rootPath rootPath
* @throws Exception ex
*/
public static void startFileMoni ( String rootPath ) throws Exception {
FileAlterationObserver observer = new FileAlterationObserver ( rootPath );
observer . addListener ( new FileListener ());
FileAlterationMonitor monitor = new FileAlterationMonitor ( 500 );
monitor . addObserver ( observer );
monitor . start ();
}
}
Application类由自定义的ClassLoader加载,则在Application中实例化的对象都将有自定义的MyClassLoader加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FileListener extends FileAlterationListenerAdaptor {
@Override
public void onFileChange ( File file ) {
if ( file . getName (). contains ( CLASS_SUFFIX )) {
try {
LogUtil . logger . info ( "{} 发生更改~" , file . getName ());
// 热部署
Application . close ();
MyClassLoader myClassLoader = new MyClassLoader ( Application . rootPath , Application . rootPath + Constants . PACKAGE_PATH );
Application . start0 ( myClassLoader );
} catch ( Exception e ) {
LogUtil . logger . error ( "error. " , e );
}
}
}
}
文件监听器,class文件发生改变,则会重新加载class文件。
测试:
1
2
3
4
5
6
7
8
9
10
public class AppTest {
public static void main ( String [] args ) {
try {
Application . run ( MyClassLoader . class );
} catch ( Exception e ) {
LogUtil . logger . error ( "error." , e );
}
}
}
经过测试发现,当User中的sayHello()方法改变时,重新编译,即可执行更改后的代码,证明实现了热加载。
上述source code可在Github 上进行浏览。
5. 总结
CLassLoader中的三个重要的方法loadClass()、findLoadedClass()、defineClass()以及它们的作用。
全盘委托 和 双亲委派, 以及二者之间的区别。