java對象引用體系除了強引用之外,出于對性能,可擴充性等方面考慮還特地實作了四種其他引用:<code>softreference</code>、<code>weakreference</code>、<code>phantomreference</code>、<code>finalreference</code>,本文主要想講的是<code>finalreference</code>,因為zprofiler在分析一些oom的heap的時候,經常能看到 <code>java.lang.ref.finalizer</code>占用的記憶體大小遠遠排在前面(finalizer heap demo),而這個類占用的記憶體大小又和我們這次的主角<code>finalreference</code>及關聯的内容可能給我們留下如下印象:
自己代碼裡從沒有使用過;
線程dump之後,會看到一個叫做<code>finalizer</code>的java線程;
偶爾能注意到<code>java.lang.ref.finalizer</code>的存在;
在類裡可能會寫<code>finalize</code>方法。
那<code>finalreference</code>到底存在的意義是什麼,以怎樣的形式和我們的代碼相關聯呢?這是本文要理清的問題。
首先我們看看<code>finalreference</code>在jdk裡的實作:
大家應該注意到了類通路權限是package的,這也就意味着我們不能直接去對其進行擴充,但是jdk裡對此類進行了擴充實作<code>java.lang.ref.finalizer</code>,這個類在概述裡提到的過,而此類的通路權限也是package的,并且是final的,意味着它不能再被擴充了,接下來的重點我們圍繞<code>java.lang.ref.finalizer</code>展開。(ps:後續講的<code>finalizer</code>其實也是在說<code>finalreference</code>。)
<code>finalizer</code>的構造函數提供了以下幾個關鍵資訊:
private:意味着我們無法在目前類之外建構這類的對象;
finalizee參數:<code>finalreference</code>指向的對象引用;
調用add方法:将目前對象插入到<code>finalizer</code>對象鍊裡,鍊裡的對象和<code>finalizer</code>類靜态關聯。言外之意是在這個鍊裡的對象都無法被gc掉,除非将這種引用關系剝離(因為<code>finalizer</code>類無法被unload)。
雖然外面無法建立<code>finalizer</code>對象,但是它有一個名為register的靜态方法,該方法可以建立這種對象,同時将這個對象加入到<code>finalizer</code>對象鍊裡,這個方法是被vm調用的,那麼問題來了,vm在什麼情況下會調用這個方法呢?
類的修飾有很多,比如final,abstract,public等,如果某個類用final修飾,我們就說這個類是final類,上面列的都是文法層面我們可以顯式指定的,在jvm裡其實還會給類标記一些其他符号,比如<code>finalizer</code>,表示這個類是一個<code>finalizer</code>類(為了和<code>java.lang.ref.fianlizer</code>類區分,下文在提到的<code>finalizer</code>類時會簡稱為f類),gc在處理這種類的對象時要做一些特殊的處理,如在這個對象被回收之前會調用它的<code>finalize</code>方法。
在講這個問題之前,我們先來看下<code>java.lang.object</code>裡的一個方法
在<code>object</code>類裡定義了一個名為<code>finalize</code>的空方法,這意味着java裡的所有類都會繼承這個方法,甚至可以覆寫該方法,并且根據方法覆寫原則,如果子類覆寫此方法,方法通路權限至少protected級别的,這樣其子類就算沒有覆寫此方法也會繼承此方法。
而判斷目前類是否是f類的标準并不僅僅是目前類是否含有一個參數為空,傳回值為void的<code>finalize</code>方法,還要求<code>finalize方法必須非空</code>,是以object類雖然含有一個<code>finalize</code>方法,但它并不是f類,object的對象在被gc回收時其實并不會調用它的<code>finalize</code>方法。
需要注意的是,類在加載過程中其實就已經被标記為是否為f類了。(jvm在類加載的時候會周遊目前類的所有方法,包括父類的方法,隻要有一個參數為空且傳回void的非空<code>finalize</code>方法就認為這個類是一個f類。)
對象的建立其實是被拆分成多個步驟的,比如<code>a a=new a(2)</code>這樣一條語句對應的位元組碼如下:
先執行new配置設定好對象空間,然後再執行invokespecial調用構造函數,jvm裡其實可以讓使用者在這兩個時機中選擇一個,将目前對象傳遞給<code>finalizer.register</code>方法來注冊到<code>finalizer</code>對象鍊裡,這個選擇取決于是否設定了<code>registerfinalizersatinit</code>這個vm參數,預設值為true,也就是在構造函數傳回之前調用<code>finalizer.register</code>方法,如果通過<code>-xx:-registerfinalizersatinit</code>關閉了該參數,那将在對象空間配置設定好之後将這個對象注冊進去。
另外需要提醒的是,當我們通過clone的方式複制一個對象時,如果目前類是一個f類,那麼在clone完成時将調用<code>finalizer.register</code>方法進行注冊。
這個實作比較有意思,在這簡單提一下,我們知道執行一個構造函數時,會去調用父類的構造函數,主要是為了初始化繼承自父類的屬性,那麼任何一個對象的初始化最終都會調用到<code>object</code>的空構造函數裡(任何空的構造函數其實并不空,會含有三條位元組碼指令,如下代碼所示),為了不對所有類的構造函數都埋點調用<code>finalizer.register</code>方法,hotspot的實作是,在初始化<code>object</code>類時将構造函數裡的<code>return</code>指令替換為<code>_return_register_finalizer</code>指令,該指令并不是标準的位元組碼指令,是hotspot擴充的指令,這樣在處理該指令時調用<code>finalizer.register</code>方法,以很小的侵入性代價完美地解決了這個問題。
在<code>finalizer</code>類的clinit方法(靜态塊)裡,我們看到它會建立一個<code>finalizerthread</code>守護線程,這個線程的優先級并不是最高的,意味着在cpu很緊張的情況下其被排程的優先級可能會受到影響
這個線程用來從queue裡擷取<code>finalizer</code>對象,然後執行該對象的<code>runfinalizer</code>方法,該方法會将<code>finalizer</code>對象從<code>finalizer</code>對象鍊裡剝離出來,這樣意味着下次gc發生時就可以将其關聯的f對象回收了,最後将這個<code>finalizer</code>對象關聯的f對象傳給一個native方法<code>invokefinalizemethod</code>
其實<code>invokefinalizemethod</code>方法就是調了這個f對象的finalize方法,看到這裡大家應該恍然大悟了,整個過程都串起來了。
不知道大家有沒有想過如果f對象的<code>finalize</code>方法抛了一個沒捕獲的異常,這個<code>finalizerthread</code>會不會退出呢,細心的讀者看上面的代碼其實就可以找到答案,<code>runfinalizer</code>方法裡對<code>throwable</code>的異常進行了捕獲,是以不可能出現<code>finalizerthread</code>因異常未捕獲而退出的情況。
如果我們在f對象的<code>finalize</code>方法裡重新将目前對象指派,變成可達對象,當這個f對象再次變成不可達時還會執行<code>finalize</code>方法嗎?答案是否定的,因為在執行完第一次<code>finalize</code>方法後,這個f對象已經和之前的<code>finalizer</code>對象剝離了,也就是下次gc的時候不會再發現<code>finalizer</code>對象指向該f對象了,自然也就不會調用這個f對象的<code>finalize</code>方法了。
除了這裡接下來要介紹的環節之外,整個過程大家應該都比較清楚了。
當gc發生時,gc算法會判斷f類對象是不是隻被<code>finalizer</code>類引用(f類對象被<code>finalizer</code>對象引用,然後放到<code>finalizer</code>對象鍊裡),如果這個類僅僅被<code>finalizer</code>對象引用,說明這個對象在不久的将來會被回收,現在可以執行它的<code>finalize</code>方法了,于是會将這個<code>finalizer</code>對象放到<code>finalizer</code>類的<code>referencequeue</code>裡,但是這個f類對象其實并沒有被回收,因為<code>finalizer</code>這個類還對它們保持引用,在gc完成之前,jvm會調用<code>referencequeue</code>中lock對象的notify方法(當<code>referencequeue</code>為空時,<code>finalizerthread</code>線程會調用<code>referencequeue</code>的lock對象的wait方法直到被jvm喚醒),此時就會執行上面finalizethread線程裡看到的其他邏輯了。
這裡舉一個簡單的例子,我們使用挺廣的socket通信,<code>sockssocketimpl</code>的父類其實就實作了<code>finalize</code>方法:
其實這麼做的主要目的是萬一使用者忘記關閉socket,那麼在這個對象被回收時能主動關閉socket來釋放一些系統資源,但是如果使用者真的忘記關閉,那這些socket對象可能因為<code>finalizethread</code>遲遲沒有執行這些socket對象的finalize方法,而導緻記憶體洩露,這種問題我們碰到過多次,需要特别注意的是對于已經沒有地方引用的這些f對象,并不會在最近的那一次gc裡馬上回收掉,而是會延遲到下一個或者下幾個gc時才被回收,因為執行finalize方法的動作無法在gc過程中執行,萬一finalize方法執行很長呢,是以隻能在這個gc周期裡将這個垃圾對象重新标活,直到執行完finalize方法從queue裡删除,這樣下次gc的時候就真的是漂浮垃圾了會被回收,是以給大家的一個建議是千萬不要在運作期不斷建立f對象,不然會很悲劇。
上面的過程基本對finalizer的實作細節進行了完整剖析,java裡我們看到有構造函數,但是并沒有看到析構函數一說,<code>finalizer</code>其實是實作了析構函數的概念,我們在對象被回收前可以執行一些“收拾性”的邏輯,應該說是一個特殊場景的補充,但是這種概念的實作給f對象生命周期以及gc等帶來了一些影響:
f對象因為<code>finalizer</code>的引用而變成了一個臨時的強引用,即使沒有其他的強引用,還是無法立即被回收;
f對象至少經曆兩次gc才能被回收,因為隻有在<code>finalizerthread</code>執行完了f對象的<code>finalize</code>方法的情況下才有可能被下次gc回收,而有可能期間已經經曆過多次gc了,但是一直還沒執行f對象的<code>finalize</code>方法;
cpu資源比較稀缺的情況下<code>finalizerthread</code>線程有可能因為優先級比較低而延遲執行f對象的<code>finalize</code>方法;
因為f對象的<code>finalize</code>方法遲遲沒有執行,有可能會導緻大部分f對象進入到old分代,此時容易引發old分代的gc,甚至full gc,gc暫停時間明顯變長;
f對象的<code>finalize</code>方法被調用後,這個對象其實還并沒有被回收,雖然可能在不久的将來會被回收。
該文章來自于阿裡巴巴技術協會(ata)精選文章
個人公衆号:
