沒有被回收的對象
上篇文章介紹了自己寫的延遲隊列工具。我們提到,延遲隊列不需要長久存活,我們使用帶有lru功能的LinkedHashMap來淘汰一些不常用的LimitUtil。但是對象有沒有真的會回收呢?
簡單寫了一個測試類,建了三個對象,Lru容量設爲1
LRU<String, LimitUtil> map = new LRU<>(1, 0.75f);
@Test
public void test() throws InterruptedException {
LimitUtilFactory limitUtilFactory = new LimitUtilFactory();
for (int i = 0; i < 3; i++) {
int finalI = i;
LimitUtil instance = limitUtilFactory.getInstance(String.valueOf(finalI), 10, 10, 10, (x) -> {
log.info("執行邏輯 {}", x);
});
instance.put("LimitUtilTest test" + finalI);
}
Thread.sleep(1000000000);
}
我們看下VisualVm的堆快照,第一個對象是被LinkedHashMap所引用
另外兩個對象都沒有被LinkedHashMap引用
但是我們手動點擊GC,堆裏LimitUtil的數量依然是三個,我們的程序已經無法取到LimitUtil的實例,但是堆裏的對象沒有被回收,這裏發生了內存泄漏。
GC Root
我們如果看了垃圾回收的書,我們可以瞭解到一個對象決定回不回收有兩種算法,一種引用計數算法
,一種可達性分析算法(GC Root)
,可達性就是給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的。這種算法 有一個致命問題,如果兩個對象相互引用,那就沒法判斷它們是否真的需要被回收,所以此算法已被淘汰。
可達性分析算法
則換了種思路,就是通過一系列的稱爲“GC Roots”的對象作爲起始點, 從這些節點開始向下搜索, 搜索所走過的路徑稱爲引用鏈( Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。GC會收集那些不是GC Roots且沒有被GC Roots引用的對象。
那什麼可以作爲“GC Roots”的對象作爲起始點?
- Class - 由系統類加載器(system class loader)加載的對象,這些類是不能夠被回收的,他們可以以靜態字段的方式保存持有其它對象。我們需要注意的一點就是,通過用戶自定義的類加載器加載的類,除非相應的Java.lang.Class實例以其它的某種(或多種)方式成爲roots,否則它們並不是roots
- Thread - 活着的線程
- Stack Local - Java方法的local變量或參數
- JNI Local - JNI方法的local變量或參數
- JNI Global - 全局JNI引用
- Monitor Used - 用於同步的監控對象
- Held by JVM - 用於JVM特殊目的由GC保留的對象,但實際上這個與JVM的實現是有關的。可能已知的一些類型是:系統類加載器、一些JVM知道的重要的異常類、一些用於處理異常的預分配對象以及一些自定義的類加載器等。然而,JVM並沒有爲這些對象提供其它的信息,因此需要去確定哪些是屬於"JVM持有"的了。
講到這裏,是不是覺得LimitUtil即使沒有被 LinkedHashMap引用,但是,是否會存在GC Root
對象而導致自身無法回收? 我們再回顧VisualVm的堆快照引用分析。 我們可以看見VisualVm把GC Root對象都用藍色三角
修飾了,再展開看引用分析, 我們的兩個ThreadPool都被虛擬機當作GC Root對象,而不可回收。
在LimitUtil的實現裏,我們都用了ThreadPoolExecutor,而且當沒有元素put進去的時候,線程會掛起等待,貌似符合gc root對象所定義的活的線程,目標有了,我們需要將這個線程池關閉。對於ThreadPoolExecutor的源碼,可以推薦看下面的文章:
【死磕Java併發】—–J.U.C之線程池:ThreadPoolExecutor
shutdownNow方法可以關閉ThreadPoolExecutor內部活躍的線程。我們在LinkedHashMap的removeEldestEntry方法裏面寫一個關閉方法。保證LinkedHashMap丟棄這個對象的時候可以執行對應的清理代碼。
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 當元素個數大於了緩存的容量, 就移除元素
if (size() > this.capacity) {
LimitUtil value = (LimitUtil) eldest.getValue();
value.close();
}
return size() > this.capacity;
}
public void close() {
service.shutdownNow();
executorService.shutdownNow();
}
做完之後,我們重新執行程序,在執行gc前,我們看看VisualVm的堆快照。藍色三角形沒有了。這個對象理論上只剩下被回收。
我們手動執行一遍gc,LimitUtil只剩下1個。對象被成功回收!
其它收穫,Finalizer
在VisualVm堆快照中,我發現有些對象會被referent引用。通過查閱資料,瞭解到所有覆寫了finalize方法的對象在創建時 都會被Finalizer包裝並且 在引用不可達時放入ReferenceQueue中,隊列會執行對象的finalize方法,所以以前只知道finalize方法是 對象被gc前執行的方法,現在也知道了前因後果。
Finalizer跟WeakReference(弱引用),SoftReference(軟引用) 同系一脈,適合當緩存。感興趣的同學可以深入瞭解。