前兩天,項目中發現一個Bug。咱們使用的 RocketMQ
,在服務啓動後會建立MQ的消費者實例,來訂閱topic。測試過程當中,發現服務啓動一段時間後,與 RocketMQ
的鏈接就會斷掉,從而找不到訂閱關係,監聽不到數據。服務器
通過回溯代碼,發現訂閱的邏輯是這樣的。將 ConsumerStarter
類註冊到Spring,並經過 PostConstruct
註解觸發初始化方法,完成MQ消費者的建立和訂閱。工具
上面代碼中的 Subscriber
類是同事寫的一個工具類,訂閱的時候都調用這裏。這裏面也不復雜,就是調用 RocketMQ
,完成建立和訂閱。測試
上面的代碼看起來平平無奇,但實際上他重寫了 finalize
方法。而且在裏面執行了 consumer.shutdown()
,將 RocketMQ
斷開了,這裏是誘因。線程
finalize
是 Object
中的方法。在GC(垃圾回收器)決定回收一個不被其餘對象引用的對象時調用。子類覆寫 finalize
方法來處置系統資源或是負責清除操做。code
回到項目中,他這樣的寫法就是在 Subscriber
類被回收的時候,斷開 RokcketMQ
的鏈接,於是產生了Bug。最簡單的方式就是把 shutdown
這句代碼刪掉,但這彷佛不是好的解決方案。cdn
在Java的內存模型中,有一個 虛擬機棧
,它是線程私有的。對象
虛擬機棧是線程私有的,每建立一個線程,虛擬機就會爲這個線程建立一個虛擬機棧,虛擬機棧表示Java方法執行的內存模型,每調用一個方法就會爲每一個方法生成一個棧幀(Stack Frame),用來存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法被調用和完成的過程,都對應一個棧幀從虛擬機棧上入棧和出棧的過程。虛擬機棧的生命週期和線程是相同的blog
在上面的 ConsumerStarter.init()
方法中, Subscriber subscriber = new Subscriber()
被定義成了局部變量,在方法執行完畢後,變量就沒有了引用,會被銷燬。生命週期
很快,我就有了新的想法,將 Subscriber
定義成 ConsumerStarter
類中的成員變量也是能夠的,由於 ConsumerStarter
是註冊到了 Spring
中。在Bean的生命週期內,不會被回收。內存
如上代碼,把 subscriber
做用域提到類級別,事實證實這樣也是沒問題的。
還有個更優的方案是,將 Subscriber
直接註冊到 Spring
中,由 PostConstruct
註解觸發初始化完成對MQ的建立和訂閱;由 PreDestroy
註解完成資源的釋放。這樣,資源的建立和銷燬跟Bean的生命週期綁定,也是沒問題的。
到目前爲止,這個Bug的緣由和解決方案都有了。但還有個問題,筆者一時沒想明白。
爲了肯定哪些對象是垃圾,在Java中使用了可達性分析的方法。
它經過經過一系列的 GC roots
對象做爲起點搜索。從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。
在Java語言中,可做爲GC Roots的對象包括下面幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中類靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI(即通常說的Native方法)引用的對象。
結合代碼來看,虛擬機棧中引用的對象是 subscriber
,而 subscriber
對象中又包含了 Consumer
對象。 Consumer
對象是在 RocketMQ
中建立的,而且調用了它的 consumer.start
方法。
我大概看了下 RocketMQ
,做爲一個 Consumer
實例,它確定會按期從 Name Server 拉取消息;而且定時向服務器發生心跳。並且在 RocketMQ
代碼中,我也看到了 ScheduledExecutorService
這種定時器的啓動。
那麼,這一切說明, subscriber
類的 consumer
的實例是活躍的呀,它們之間是可達的,不該該被回收吧?
這個問題也能夠被描述成:若是A對象沒有了引用,是肯定能夠被回收的 好比局部變量subscriber,方法執行完應該就被銷燬
;可是若是A對象中還有線程在活躍, 好比在活躍的線程是consumer實例
,此時A對象還會被回收嗎?
而後,基於上面的問題,筆者又作了兩個測試。
回到上面項目中的代碼,此時我仍是將 Subscriber
定義成局部變量,這樣在GC的時候,它仍是要被回收的。在這裏,能夠經過 System.gc();
來手動觸發GC。
在 Subscriber
類中,經過 new Thread().start();
的方式來建立一個線程並調用它的啓動方法,總體代碼以下:
若是是這種狀況,當觸發GC的時候, Subscriber
類不會被回收, finalize
方法也沒有被調用,線程還會持續輸出。
首先定義一個線程類 MyThread1
,它的run方法也是死循環。
而後在 Subscriber
類中經過 MyThread1 thread1 = new MyThread1();
實例化。
而後經過 new Thread(thread1).start();
來啓動它。
此時,若是觸發GC, Subscriber
類照樣會被回收, finalize
方法也會被調用,但 thread1
線程仍然還會持續輸出。
經過這兩個測試,我更不太明白了。都是在 Subscriber
類中啓動新的線程,爲何結果卻不一樣呢?
是由於在測試1中,本類的線程還未執行結束,方法未結束嗎?