前兩天,項目中發現一個Bug。咱們使用的RocketMQ
,在服務啓動後會建立MQ
的消費者實例。測試過程當中,發現服務啓動一段時間後,與RocketMQ
的鏈接就會斷掉,從而找不到訂閱關係,監聽不到數據。bash
通過回溯代碼,發現訂閱的邏輯是這樣的。將ConsumerStarter
類註冊到Spring,並經過PostConstruct
註解觸發初始化方法,完成MQ
消費者的建立和訂閱。 微信
上面代碼中的Subscriber
類是同事寫的一個工具類,封裝了一些鏈接RocketMQ
的配置信息,使用的時候都調用這裏。這裏面也不復雜,就是鏈接RocketMQ
,完成建立和訂閱。app
上面的代碼看起來平平無奇,但實際上他重寫了finalize
方法。而且在裏面執行了consumer.shutdown()
,將RocketMQ
斷開了,這裏是誘因。工具
finalize
是Object
中的方法。在GC(垃圾回收器)決定回收一個不被其餘對象引用的對象時調用。子類覆寫 finalize
方法來處置系統資源或是負責清除操做。測試
回到項目中,他這樣的寫法就是在Subscriber
類被回收的時候,斷開RokcketMQ
的鏈接,於是產生了Bug。最簡單的方式就是把shutdown
這句代碼刪掉,但這彷佛不是好的解決方案。this
在Java的內存模型中,有一個虛擬機棧
,它是線程私有的。spa
虛擬機棧是線程私有的,每建立一個線程,虛擬機就會爲這個線程建立一個虛擬機棧,虛擬機棧表示Java方法執行的內存模型,每調用一個方法就會爲每一個方法生成一個棧幀(Stack Frame),用來存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法被調用和完成的過程,都對應一個棧幀從虛擬機棧上入棧和出棧的過程。虛擬機棧的生命週期和線程是相同的.net
在上面的ConsumerStarter.init()
方法中,Subscriber subscriber = new Subscriber()
被定義成了局部變量,在方法執行完畢後,subscriber
變量就沒有了引用,而後GC掉。線程
很快,我就有了新的想法,將Subscriber
定義成ConsumerStarter
類中的成員變量也是能夠的,由於ConsumerStarter
是註冊到了Spring
中。在Bean的生命週期內,不會被回收。3d
如上代碼,把subscriber
變量做用域提到類級別,事實證實這樣也是沒問題的。
還有個更優的方案是,將Subscriber
類直接註冊到Spring
中,由PostConstruct
註解觸發初始化完成對MQ的建立和訂閱;由PreDestroy
註解完成資源的釋放。這樣,資源的建立和銷燬跟Bean的生命週期綁定,也是沒問題的。
到目前爲止,這個Bug的緣由和解決方案都有了。但還有個問題,筆者當時也沒想明白。
爲了肯定哪些對象是垃圾,在Java中使用了可達性分析的方法。
它經過經過一系列的GC roots
對象做爲起點搜索。從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。
在上面的例子中,雖然Subscriber
類已經不被引用,要被回收,可是它裏面還有RocketMQ
的Consumer
實例還在以線程的方式運行,這種狀況下,Subscriber
類也會被回收掉嗎?
那麼,就變成了這樣一個問題: 若是一個對象A已不被引用,符合GC條件;可是它裏面還有一個線程對象a1在活躍,那麼此時這個對象A還會被回收嗎?
爲了驗證這個問題,筆者作了一個測試。
在這裏,仍是以Subscriber
類爲例,給它啓動一個線程,看是否還會被GC。簡化代碼以下:
測試流程是:啓動服務 > Subscriber類被建立 > 線程執行10次循環 > 手動調用GC
得出結果以下:
15:01:07.110 [main] INFO - Subscriber已啓動...
15:01:07.112 [Thread-9] INFO - 執行線程次數:0
15:01:07.357 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
15:01:07.568 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
15:01:07.609 [main] INFO - Tomcat started on port(s): 8081 (http) with context path ''
15:01:07.614 [main] INFO - Started BootMybatisApplication in 3.654 seconds
15:01:08.114 [Thread-9] INFO - 執行線程次數:1
15:01:09.114 [Thread-9] INFO - 執行線程次數:2
15:01:09.302 [http-nio-8081-exec-2] INFO - 調用GC。。。
15:01:10.122 [Thread-9] INFO - 執行線程次數:3
15:01:11.123 [Thread-9] INFO - 執行線程次數:4
15:01:12.123 [Thread-9] INFO - 執行線程次數:5
15:01:12.319 [http-nio-8081-exec-3] INFO - 調用GC。。。
15:01:13.123 [Thread-9] INFO - 執行線程次數:6
15:01:14.124 [Thread-9] INFO - 執行線程次數:7
15:01:15.125 [Thread-9] INFO - 執行線程次數:8
15:01:15.319 [http-nio-8081-exec-5] INFO - 調用GC。。。
15:01:16.125 [Thread-9] INFO - 執行線程次數:9
15:01:17.775 [http-nio-8081-exec-7] INFO - 調用GC。。。
15:01:17.847 [Finalizer] INFO - finalize-------------Subscriber對象被銷燬
複製代碼
從結果上看,若是Subscriber
類若是有活躍的線程在運行,它是不會被回收的;等線程運行完以後,再次調用GC,纔會被回收掉。不過,先不要急着下結論,咱們再測試一下別的狀況。
此次,咱們先建立一個線程類Thread1
。
它的run方法,咱們跟上面保持一致。而後在Subscriber
對象中經過線程調用它。
流程和測試1同樣,此次的結果輸出以下:
14:59:20.193 [main] INFO - Subscriber已啓動...
14:59:20.194 [Thread-9] INFO - 執行次數:0
14:59:20.359 [Finalizer] INFO - finalize-------------Subscriber對象被銷燬
14:59:20.444 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
14:59:20.699 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
14:59:20.745 [main] INFO - Tomcat started on port(s): 8081 (http) with context path ''
14:59:20.751 [main] INFO - Started BootMybatisApplication in 3.453 seconds
14:59:21.197 [Thread-9] INFO - 執行次數:1
14:59:22.198 [Thread-9] INFO - 執行次數:2
14:59:23.198 [Thread-9] INFO - 執行次數:3
14:59:24.198 [Thread-9] INFO - 執行次數:4
14:59:25.198 [Thread-9] INFO - 執行次數:5
14:59:26.198 [Thread-9] INFO - 執行次數:6
14:59:27.198 [Thread-9] INFO - 執行次數:7
14:59:28.199 [Thread-9] INFO - 執行次數:8
14:59:29.199 [Thread-9] INFO - 執行次數:9
複製代碼
從結果上看,Subscriber
建立以後就由於ConsumerStarter.init()
方法執行完畢,而被銷燬了,絲毫沒有受線程的影響。
咦,這就有意思了。從邏輯上看,兩個測試都是經過new Thread()
開啓了新的線程,爲啥結果卻不同呢?
立即就在微信聯繫了芋道源碼大神,大神果真老道,他一眼指出:你這個應該是內部類的問題。
咱們把目光回到測試1的代碼中。
若是在方法中,這樣建立一個線程,實際上建立了一個匿名內部類。可是,它有什麼特殊之處嗎?居然會阻止對象會正常回收。
咱們知道在內部類編譯成功後,它會產生一個class文件,該class文件與外部類並非同一class文件。在上面的代碼中,編譯後就產生一個class文件叫作:Subscriber$1.class
。
經過反編譯軟件,咱們能夠獲得這個class文件的內容以下:
看到這裏,就已經很明確了。這個內部類雖然與外部類不是同一個class文件,可是它保留了對外部類的引用,就是這個Subscriber this$0
。
只要內部類的方法執行不完,就會還保留外部類的引用實例,因此外部類就不會被GC回收。
因此,再回到一開始的問題:
若是一個對象A已不被引用,符合GC條件;可是它裏面還有一個線程對象a1在活躍,那麼此時這個對象A還會被回收嗎?
答案也是確定的,除非線程對象a1還在引用A對象。
第二部分中,咱們看的是匿名內部類中開啓線程和垃圾回收的關係,咱們再看看與線程無關的狀況。
在乎識到是由於匿名內部類致使垃圾回收的問題時,在網上找到一篇文章。說的也是匿名內部類垃圾回收的問題,原文連接: 匿名內部類相關的gc
裏面的代碼筆者也測試過了,確實也如原文做者所說:
從結果推測,匿名內部類會關聯對象,內部類對象不回收,致使主對象沒法回收;
但實際上,這並非匿名內部類的問題,或者說不徹底是它的問題,而是static
修飾符的問題。
使用匿名內部類咱們必需要繼承一個父類或者實現一個接口,因此咱們隨便建立一個接口。
public interface TestInterface {
void out();
}
複製代碼
而後在Subscriber
外部類中使用它。
而後啓動服務,輸出結果以下:
16:37:53.626 [main] INFO - Root WebApplicationContext: initialization completed in 1867 ms
16:37:53.710 [main] INFO - Subscriber已啓動...
16:37:53.711 [main] INFO - 匿名內部類測試...
16:37:53.866 [Finalizer] INFO - finalize-------------Subscriber對象被銷燬
16:37:53.950 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
16:37:54.180 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
16:37:54.224 [main] INFO - Tomcat started on port(s): 8081 (http) with context path ''
複製代碼
從結果來看,Subscriber
對象被建立,使用完以後,不須要手動調用GC
,就被銷燬了。
若是將interface1
變量定義成靜態的呢?static TestInterface interface1;
輸出結果以下:
16:43:20.331 [main] INFO - Root WebApplicationContext: initialization completed in 1826 ms
16:43:20.404 [main] INFO - Subscriber已啓動...
16:43:20.405 [main] INFO - 匿名內部類測試...
16:43:20.673 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
16:43:20.955 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
16:43:21.002 [main] INFO - Tomcat started on port(s): 8081 (http) with context path ''
16:43:21.007 [main] INFO - Started BootMybatisApplication in 3.327 seconds
16:43:33.095 [http-nio-8081-exec-1] INFO - Initializing Servlet 'dispatcherServlet'
16:43:33.102 [http-nio-8081-exec-1] INFO - Completed initialization in 7 ms
16:43:33.140 [http-nio-8081-exec-1] INFO - 調用GC。。。
16:43:35.763 [http-nio-8081-exec-2] INFO - 調用GC。。。
複製代碼
若是把interface1
定義成靜態的,再怎麼調用GC
,Subscriber
都不會被回收掉。
怎麼辦呢?能夠在interface1
使用完以後,把它從新賦值成空。interface1=null;
,這時候,Subscriber
類又會像測試1裏面那樣,被正常回收掉。
不過,這又是爲何呢?Java的垃圾回收是依靠GC Roots
開始向下搜索的,當一個對象到GC Roots
沒有任何引用鏈相連時,則證實此對象是不可用的;那麼反之,就是可用的,不可回收的。
GC Roots
包含如下對象:
也就是說,若是咱們把匿名內部類interface1
定義成靜態的對象,它就會看成GC Root
對象存在,此時,這個內部類裏面又保持着外部類Subscriber
的引用,因此遲遲不能被回收。
爲何說,這並非匿名內部類的問題,或者說不徹底是它的問題,而是static修飾符的問題,咱們能夠再作一個測試。
關於匿名內部類,咱們先無論它是如何產生的,也不理會又是怎麼用的,咱們只記住它的特性之一:
它會保持外部類的引用。
因此,咱們先摒棄什麼勞什子內部類,就建立一個普通的類,讓它保持調用類的引用。
而後在外部類Subscriber
中,建立它的實例,並調用:
最後啓動服務,輸出結果以下:
17:30:02.331 [main] INFO - Root WebApplicationContext: initialization completed in 1726 ms
17:30:02.443 [main] INFO - Subscriber已啓動...
com.viewscenes.netsupervisor.controller.test.Subscriber@7ea899a9
17:30:02.447 [main] INFO - User對象輸出信息....
com.viewscenes.netsupervisor.controller.test.Subscriber@7ea899a9
17:30:02.605 [Finalizer] INFO - finalize-------------Subscriber對象被銷燬
17:30:02.606 [Finalizer] INFO - finalize-------------User對象被銷燬
17:30:02.676 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
17:30:02.880 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
複製代碼
這種狀況下,都是會正常被回收的。
但若是這時候再把user
對象定義成靜態屬性,無論怎麼GC
,仍是不能回收。
由此看來,能夠得出這樣一個結論:
本文經過一個小Bug,引起了內存模型和垃圾回收的問題。由於基礎理論知識還不過硬,只能經過不一樣的測試,來解釋整個事件的緣由。
另外,雖然Java幫咱們自動回收垃圾,但你們寫代碼的時候注意不要引起內存泄露哦。