在給熱部署系統熱加載資源時,如果不掌握對象和資源的生命週期,系統運行時很容易產生一些意想不到的錯誤。從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。
總結
一個對象,一個被加載的資源的都有它的生命週期,開發時不注意他們的生命週期,系統越滾越大時,一些像見了鬼的問題便會冒出來找你玩。
堅持每個月至少兩篇博客的習慣,積攢些跬步,小流之功。