【求解惑】由一個Bug來看Java內存模型和垃圾回收


前兩天,項目中發現一個Bug。咱們使用的 RocketMQ ,在服務啓動後會建立MQ的消費者實例,來訂閱topic。測試過程當中,發現服務啓動一段時間後,與 RocketMQ 的鏈接就會斷掉,從而找不到訂閱關係,監聽不到數據。服務器

1、Bug的產生

通過回溯代碼,發現訂閱的邏輯是這樣的。將 ConsumerStarter 類註冊到Spring,並經過 PostConstruct 註解觸發初始化方法,完成MQ消費者的建立和訂閱。工具

上面代碼中的 Subscriber 類是同事寫的一個工具類,訂閱的時候都調用這裏。這裏面也不復雜,就是調用 RocketMQ ,完成建立和訂閱。測試

一、finalize

上面的代碼看起來平平無奇,但實際上他重寫了 finalize 方法。而且在裏面執行了 consumer.shutdown() ,將 RocketMQ 斷開了,這裏是誘因。線程

finalizeObject 中的方法。在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的緣由和解決方案都有了。但還有個問題,筆者一時沒想明白。

2、疑問點

爲了肯定哪些對象是垃圾,在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類中新建線程

Subscriber 類中,經過 new Thread().start(); 的方式來建立一個線程並調用它的啓動方法,總體代碼以下:

若是是這種狀況,當觸發GC的時候, Subscriber 類不會被回收, finalize 方法也沒有被調用,線程還會持續輸出。

二、在Subscriber類中調用其餘線程類

首先定義一個線程類 MyThread1 ,它的run方法也是死循環。

而後在 Subscriber 類中經過 MyThread1 thread1 = new MyThread1(); 實例化。

而後經過 new Thread(thread1).start(); 來啓動它。

此時,若是觸發GC, Subscriber 類照樣會被回收, finalize 方法也會被調用,但 thread1線程仍然還會持續輸出。

經過這兩個測試,我更不太明白了。都是在 Subscriber 類中啓動新的線程,爲何結果卻不一樣呢?

是由於在測試1中,本類的線程還未執行結束,方法未結束嗎?

請大佬們帶着批判的目光審視第二部分,其中邏輯可能有誤,請大佬們不吝賜教。若是一兩句話扯不清楚,也但願有大佬能夠專門寫篇文章講講這裏面的邏輯誤區~

相關文章
相關標籤/搜索