在给热部署系统热加载资源时,如果不掌握对象和资源的生命周期,系统运行时很容易产生一些意想不到的错误。从Jar 加载到系统中,到被替换,不同的资源生命周期是不一样的。
首先是对象,如果一个对象没有手动的丢弃,那么它的生命周期与加载它的 ClassLoader 生命周期一样。销毁对象时,只需要确保对象的引用为 null 即可,清理游离的对象的工作由GC处理。
其次是静态资源,被加载的静态资源的生命周期与系统的生命周期一样,对象的销毁时,静态资源并不能被一起被销毁。因此,在销毁对象前,一定要将静态变量的引用对象置为null,否则,被引用的对象将不能被GC处理。
然后是 NativeLibrary, 本地库又称为动态库 DLL(Dynamic Link Library),通过Java平台提供的API——System.load(dllPath)、System.loadLibrary(libname) 加载进系统。它的生命周期和系统一样,虽然 ClassLoader 保存了对 NativeLibrary 的引用,但是ClassLoader的销毁,NativeLibrary 也不会被销毁。
因此,热部署时,销毁对象只需要对其引用置为 null 即可,而静态资源只需要把它对对象的引用给释放掉, 但是对于 NativeLibrary, Java 没有现成的 API 将其销毁。但是,可以通过反射销毁 NativeLibrary 资源。
通过 System.load() 加载的 NativeLibrary 对象被保存在被调用对象的 ClassLoader 中。
// Native libraries associated with the class loader.
private Vector<NativeLibrary> nativeLibraries = new Vector<>();
卸载代码如下:
public abstract class NativeLibUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(NativeLibUtils.class);
/**
* Unload native libs.
*
* @param clazz The class loaded with libs.
*/
public static void unloadNativeLibs(Class clazz) {
try {
ClassLoader classLoader = clazz.getClassLoader();
Field field = ClassLoader.class.getDeclaredField("nativeLibraries");
field.setAccessible(true);
Vector<Object> libs = (Vector<Object>) field.get(classLoader);
for (Object object : libs) {
Method finalize = object.getClass().getDeclaredMethod("finalize");
finalize.setAccessible(true);
finalize.invoke(object);
}
} catch (Exception e) {
LOGGER.warn("Unloading lib failed before destroy the instance {}.", clazz.getName(), e);
}
}
}
后续问题
理想的方式是,热部署时,将对象,静态资源,Native Library 加载进系统,而卸载时,则将所有的资源都销毁,或者保持其干净的引用,以便下次加载能有一个纯净的环境。手动的卸载DLL并不是一个完整的过程,更像是一种亡羊补牢的方式,但是能补上,能让系统稳定运行,也为时不晚。下面来说说动态库的加载和卸载过程。
加载动态库:
1,在构造器中加载动态库
这种方式比较适合单例,仅需一次加载动态库,多次调用动态库中的方法。
2,在静态块中加载动态库。
这种方式比较适合多例,因为静态块中的代码仅在第一次初始化执行,多次实例化也仅加载动态库一次。这也同样适合单例。
如果没有正确的使用上述两种方式,那么程序很可能出错。
如果使用构造器加载动态库,而没有保证其实例为单例,那么在创建实例时,程序会抛出重复加载类库的异常;
如果使用静态块加载动态库,程序又调用卸载了动态库的方法,那么之后实例化的对象是不会再次执行静态块中加载动态库的代码的,这将导致对动态库的调用出错,甚至时程序崩溃。
因此,无论时通过构造器还是静态块加载动态库,都一定要保证动态库的存在,而且仅被系统加载一次。如果有需求重新加载动态库,则一定要再重新加载前卸载干净之前被加载的动态库。
卸载动态库:
说完了动态库的加载,再说说如何在销毁前卸载动态库。这里有3种方式:
1, 在Spring 框架中,在方法上注释上 @PreDestroy。(推荐)
该方式会在该类的Bean销毁前执行被 @PreDestroy 注释的方法。因为在Spring容器中,Java Bean默认都是单例,因此,在该注释的方法中执行卸载动态库,就能保证动态库仅仅只被卸载一次。保证系统环境干净的同时,也不会导致程序出错。
@PreDestroy
private void unloadNativeLibs() {
NativeLibUtils.unloadNativeLibs(SerialPort.class); // SerialPort.class 是对动态库的封装类。
}
2, 重写finalize方法。
finalize 是 JVM 即将回收内存时,调用的方法,以此来销毁淘汰的对象。但是这就将执行卸载 DLL 动作交给了垃圾回收器,用户并不能知道 DLL 的卸载情况,这仍然可能导致重复加载类库的错误。
@Override
protected void finalize() throws Throwable {
NativeLibUtils.unloadNativeLibs(SerialPort.class);
super.finalize();
}
3, 在关闭对DLL调用时,主动调用方法卸载 NativeLibrary。
总结
一个对象,一个被加载的资源的都有它的生命周期,开发时不注意他们的生命周期,系统越滚越大时,一些像见了鬼的问题便会冒出来找你玩。
坚持每个月至少两篇博客的习惯,积攒些跬步,小流之功。