前兩天,項目中發(fā)現(xiàn)一個Bug。我們使用的?RocketMQ?,在服務(wù)啟動后會創(chuàng)建MQ的消費者實例,來訂閱topic。測試過程中,發(fā)現(xiàn)服務(wù)啟動一段時間后,與?RocketMQ?的連接就會斷掉,從而找不到訂閱關(guān)系,監(jiān)聽不到數(shù)據(jù)。
一、Bug的產(chǎn)生
經(jīng)過回溯代碼,發(fā)現(xiàn)訂閱的邏輯是這樣的。將?ConsumerStarter?類注冊到Spring,并通過?PostConstruct?注解觸發(fā)初始化方法,完成MQ消費者的創(chuàng)建和訂閱。

上面代碼中的?Subscriber?類是同事寫的一個工具類,訂閱的時候都調(diào)用這里。這里面也不復(fù)雜,就是調(diào)用?RocketMQ?,完成創(chuàng)建和訂閱。

1、finalize
上面的代碼看起來平平無奇,但實際上他重寫了?finalize?方法。并且在里面執(zhí)行了?consumer.shutdown()?,將?RocketMQ?斷開了,這里是誘因。
finalize?是?Object?中的方法。在GC(垃圾回收器)決定回收一個不被其他對象引用的對象時調(diào)用。子類覆寫?finalize?方法來處置系統(tǒng)資源或是負(fù)責(zé)清除操作。
回到項目中,他這樣的寫法就是在?Subscriber?類被回收的時候,斷開?RokcketMQ?的連接,因而產(chǎn)生了Bug。最簡單的方式就是把?shutdown?這句代碼刪掉,但這似乎不是好的解決方案。
2、為何被回收
在Java的內(nèi)存模型中,有一個?虛擬機(jī)棧?,它是線程私有的。
虛擬機(jī)棧是線程私有的,每創(chuàng)建一個線程,虛擬機(jī)就會為這個線程創(chuàng)建一個虛擬機(jī)棧,虛擬機(jī)棧表示Java方法執(zhí)行的內(nèi)存模型,每調(diào)用一個方法就會為每個方法生成一個棧幀(Stack Frame),用來存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每個方法被調(diào)用和完成的過程,都對應(yīng)一個棧幀從虛擬機(jī)棧上入棧和出棧的過程。虛擬機(jī)棧的生命周期和線程是相同的
在上面的?ConsumerStarter.init()?方法中,?Subscriber subscriber = new Subscriber()?被定義成了局部變量,在方法執(zhí)行完畢后,變量就沒有了引用,會被銷毀。
很快,我就有了新的想法,將?Subscriber?定義成?ConsumerStarter?類中的成員變量也是可以的,因為?ConsumerStarter?是注冊到了?Spring?中。在Bean的生命周期內(nèi),不會被回收。

如上代碼,把?subscriber?作用域提到類級別,事實證明這樣也是沒問題的。
還有個更優(yōu)的方案是,將?Subscriber?直接注冊到?Spring?中,由?PostConstruct?注解觸發(fā)初始化完成對MQ的創(chuàng)建和訂閱;由?PreDestroy?注解完成資源的釋放。這樣,資源的創(chuàng)建和銷毀跟Bean的生命周期綁定,也是沒問題的。
到目前為止,這個Bug的原因和解決方案都有了。但還有個問題,筆者一時沒想明白。
二、疑問點
為了確定哪些對象是垃圾,在Java中使用了可達(dá)性分析的方法。
它通過通過一系列的?GC roots?對象作為起點搜索。從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
在Java語言中,可作為GC Roots的對象包括下面幾種:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象。
方法區(qū)中類靜態(tài)屬性引用的對象。
方法區(qū)中常量引用的對象。
本地方法棧中JNI(即一般說的Native方法)引用的對象。
結(jié)合代碼來看,虛擬機(jī)棧中引用的對象是?subscriber?,而?subscriber?對象中又包含了?Consumer?對象。?Consumer?對象是在?RocketMQ?中創(chuàng)建的,并且調(diào)用了它的?consumer.start?方法。
我大概看了下?RocketMQ?,作為一個?Consumer?實例,它肯定會定期從 Name Server 拉取消息;并且定時向服務(wù)器發(fā)生心跳。而且在?RocketMQ?代碼中,我也看到了?ScheduledExecutorService?這種定時器的啟動。
那么,這一切說明,?subscriber?類的?consumer?的實例是活躍的呀,它們之間是可達(dá)的,不應(yīng)該被回收吧?
這個問題也可以被描述成:如果A對象沒有了引用,是確定可以被回收的?比如局部變量subscriber,方法執(zhí)行完應(yīng)該就被銷毀?;但是如果A對象中還有線程在活躍,?比如在活躍的線程是consumer實例?,此時A對象還會被回收嗎?
此處可能邏輯是錯誤的,也是筆者沒能理解的地方。望大佬指正、解惑。
然后,基于上面的問題,筆者又做了兩個測試。
回到上面項目中的代碼,此時我還是將?Subscriber?定義成局部變量,這樣在GC的時候,它還是要被回收的。在這里,可以通過?System.gc();?來手動觸發(fā)GC。
1、在Subscriber類中新建線程
在?Subscriber?類中,通過?new Thread().start();?的方式來創(chuàng)建一個線程并調(diào)用它的啟動方法,整體代碼如下:

如果是這種情況,當(dāng)觸發(fā)GC的時候,?Subscriber?類不會被回收,?finalize?方法也沒有被調(diào)用,線程還會持續(xù)輸出。
2、在Subscriber類中調(diào)用其他線程類
首先定義一個線程類?MyThread1?,它的run方法也是死循環(huán)。

然后在?Subscriber?類中通過?MyThread1 thread1 = new MyThread1();?實例化。
然后通過?new Thread(thread1).start();?來啟動它。
此時,如果觸發(fā)GC,?Subscriber?類照樣會被回收,?finalize?方法也會被調(diào)用,但?thread1線程仍然還會持續(xù)輸出。
通過這兩個測試,我更不太明白了。都是在?Subscriber?類中啟動新的線程,為什么結(jié)果卻不同呢?
是因為在測試1中,本類的線程還未執(zhí)行結(jié)束,方法未結(jié)束嗎?
請大佬們帶著批判的目光審視第二部分,其中邏輯可能有誤,請大佬們不吝賜教。如果一兩句話扯不清楚,也希望有大佬可以專門寫篇文章講講這里面的邏輯誤區(qū)~