您好,歡迎來到網暖!
?
當前位置:網暖 » 站長資訊 » 建站基礎 » 網絡技術 » 文章詳細 訂閱RssFeed

Java程序員必備技能內存管理機——垃圾標記

來源:網絡整理 瀏覽:179次 時間:2019-12-06
正文

1、怎么找到存活對象?

通過上篇文章我們知道,JVM創建對象時會通過某種方式從內存中劃分一塊區域進行分配。那么當我們服務器源源不斷的接收請求的時候,就會頻繁的需要進行內存分配的操作,但是我們服務器的內存確是非常有限的呢!所以對不再使用的內存進行回收再利用就成了JVM肩負的重任了! 那么,擺在JVM面前的問題來了,怎么判斷哪些內存不再使用了?怎么合理、高效的進行回收操作?既然要回收,那第一步就是要找到需要回收的對象!

1.1、引用計數法

實現思路:給對象添加一個引用計數器,每當有一個地方引用它,計數器加1。當引用失效,計數器值減1。任何時刻計數器值為0,則認為對象是不再被使用的。舉個小栗子,我們有一個People的類,People類有id和bestFriend的屬性。我們用People類來造兩個小人:

 People p1 = new People(); People p2 = new People();

通過上篇文章的知識我們知道,當方法執行的時候,方法的局部變量表和堆的關系應該是如下圖的(注意堆中對象頭中紅色括號內的數字,就是引用計數器,這里只是舉栗,實際實現可能會有差異):

Java程序員必備技能內存管理機——垃圾標記

造出來的p1和p2兩個人,我想讓他們互為最好的朋友,于是代碼如下:

 People p1 = new People(); People p2 = new People(); p1.setBestFriend(p2); p2.setBestFriend(p1);

對應的引用關系圖應該如下(注意引用計數器值的變化):

Java程序員必備技能內存管理機——垃圾標記

然后我們再做一些處理,去除變量和堆中對象的引用關系。

 People p1 = new People(); People p2 = new People(); p1.setBestFriend(p2); p2.setBestFriend(p1); p1 = null; p2 = null;

這時候引用關系圖就變成如下了,由于p1和p2對象還相互引用著,所以引用計數器的值還為1。

Java程序員必備技能內存管理機——垃圾標記

優點:實現簡單,效率高。

缺點:很難解決對象之間的相互循環引用。且開銷較大,頻繁的引用變化會帶來大量的額外運算。在談實現思路的時候有這樣一句話“任何時刻計數器值為0,則認為對象是不再被使用的”。但是通過上面的例子我們可以看到,雖然對象已經不再使用了,但計數器的值仍然是1,所以這兩個對象不會被標記為垃圾。

現狀:主流的JVM都沒有選用引用計數法來管理內存。

1.2、可達性分析

實現思路:通過GC Roots的對象作為起始點,從這些節點向下搜索,搜索走過的路徑成為引用鏈,當一個對象到GC Root沒有任何引用鏈相連時,則證明對象是不可用的。如下圖,紅色的幾個對象由于沒有跟GC Root沒有任何引用鏈相連,所以會進行標記。

Java程序員必備技能內存管理機——垃圾標記

優點:可以很好的解決對象相互循環引用的問題。

缺點:實現比較復雜;需要分析大量數據,消耗大量時間;

現狀:主流的JVM(如HotSpot)都選用可達性分析來管理內存。

2、標記死亡對象

通過可達性分析可以對需要回收的對象進行標記,是否標記的對象一定會被回收呢?并不是呢!要真正宣告一個對象的死亡,至少要經歷兩次的標記過程!

2.1、第一次標記

在可達性分析后發現到GC Roots沒有任何引用鏈相連時,被第一次標記。并且判斷此對象是否必要執行finalize()方法!如果對象沒有覆蓋finalize()方法或者finalize()已經被JVM調用過,則這個對象就會認為是垃圾,可以回收。對于覆蓋了finalize()方法,且finalize()方法沒有被JVM調用過時,對象會被放入一個成為F-Queue的隊列中,等待著被觸發調用對象的finalize()方法。

2.2、第二次標記

執行完第一次的標記后,GC將對F-Queue隊列中的對象進行第二次小規模標記。也就是執行對象的finalize()方法!如果對象在其finalize()方法中重新與引用鏈上任何一個對象建立關聯,第二次標記時會將其移出"即將回收"的集合。如果對象沒有,也可以認為對象已死,可以回收了。

finalize()方法是被第一次標記對象的逃脫死亡的最后一次機會。在jvm中,一個對象的finalize()方法只會被系統調用一次,經過finalize()方法逃脫死亡的對象,第二次不會再調用。由于該方法是在對象進行回收的時候調用,所以可以在該方法中實現資源關閉的操作。但是,由于該方法執行的時間是不確定的,甚至,在java程序不正常退出的情況下該方法都不一定會執行!所以在正常情況下,盡量避免使用!如果需要"釋放資源",可以定義顯式的終止方法,并在"try-catch-finally"的finally{}塊中保證及時調用,如File相關類的close()方法。下面我們看一個在finalize中逃脫死亡的栗子吧:

public class GCDemo { public static GCDemo gcDemo = null; public static void main(String[] args) throws InterruptedException { gcDemo = new GCDemo(); System.out.println("------------對象剛創建------------"); if (gcDemo != null) { System.out.println("我還活得好好的!"); } else { System.out.println("我死了!"); } gcDemo = null; System.gc(); System.out.println("------------對象第一次被回收后------------"); Thread.sleep(500);// 由于finalize方法的調用時間不確定(F-Queue線程調用),所以休眠一會兒確保方法完成調用 if (gcDemo != null) { System.out.println("我還活得好好的!"); } else { System.out.println("我死了!"); } gcDemo = null; System.gc(); System.out.println("------------對象第二次被回收后------------"); Thread.sleep(500); if (gcDemo != null) { System.out.println("我還活得好好的!"); } else { System.out.println("我死了!"); } // 后面無論多少次GC都不會再執行對象的finalize方法 } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("execute method finalize()"); gcDemo = this; }}

執行結果如下,具體就不多說啦,不明白的就自己動手去試試吧!

Java程序員必備技能內存管理機——垃圾標記

3、枚舉根節點

通過上面可達性分析我們了解了有哪些GC Root,了解了通過這些GC Root去搜尋并標記對象是生存還是死亡的思路。但是具體的實現就是那張圖顯示的那么簡單嗎?當然不是,因為我們的堆是分代收集的,那GC Root連接的對象可能在新生代,也可能在老年代,新生代的對象可能會引用老年代的對象,老年代的對象也可能引用新生代。如果直接通過GC Root去搜尋,則每次都會遍歷整個堆,那分代收集就沒法實現了呢!并且,枚舉整個根節點的時候是需要線程停頓的(保證一致性,不能出現正在枚舉 GC Roots,而程序還在跑的情況,這會導致 GC Roots 不斷變化,產生數據不一致導致統計不準確的情況),而枚舉根節點又比較耗時,這在大并發高訪問量情況下,分分鐘就會導致系統癱瘓!啥意思呢,下面一張圖感受一下:

Java程序員必備技能內存管理機——垃圾標記

如果是進行根節點枚舉,我們先要全棧掃描,找到變量表中存放為reference類型的變量,然后找到堆中對應的對象,最后遍歷對象的數據(如屬性等),找到對象數據中存放為指向其他reference的對象……這樣的開銷無疑是非常大的!

為解決上述問題,HotSpot 采用了一種 “準確式GC” 的技術,該技術主要功能就是讓虛擬機可以準確的知道內存中某個位置的數據類型是什么,比如某個內存位置到底是一個整型的變量,還是對某個對象的reference,這樣在進行 GC Roots枚舉時,只需要枚舉reference類型的即可。那怎么讓虛擬機準確的知道哪些位置存在的是reference類型數據呢?OopMap+RememberedSet!

OopMap記錄了棧上本地變量到堆上對象的引用關系,在GC發生時,線程會運行到最近的一個安全點停下來,然后更新自己的OopMap,記下棧上哪些位置代表著引用。枚舉根節點時,遞歸遍歷每個棧幀的OopMap,通過棧中記錄的被引用對象的內存地址,即可找到這些對象( GC Roots )。這樣,OopMap就避免了全棧掃描,加快枚舉根節點的速度。

OopMap解決了枚舉根節點耗時的問題,但是分代收集的問題依然存在!這時候就需要另一利器了- RememberedSet。對于位于不同年代對象之間的引用關系,會在引用關系發生時,在新生代邊上專門開辟一塊空間記錄下來,這就是RememberedSet!所以“新生代的 GC Roots ” + “ RememberedSet存儲的內容”,才是新生代收集時真正的GC Roots(G1 收集器也使用了 RememberedSet 這種技術)。

3.1、安全點

HotSpot在OopMap的幫助下可以快速且準確的完成GC Roots枚舉,但是在運行過程中,非常多的指令都會導致引用關系變化,如果為這些指令都生成對應的OopMap,需要的空間成本太高。所以只在特定的位置記錄OopMap引用關系,這些位置稱為安全點(Safepoint)。如何在GC發生時讓所有線程(不包括JNI線程)運行到其所在最近的安全點上再停頓下來?這里有兩種方案:

1、搶先式中斷:不需要線程的執行代碼去主動配合,當發生GC時,先強制中斷所有線程,然后如果發現某些線程未處于安全點,那么將其喚醒,直至其到達安全點再次將其中斷。這樣一直等待所有線程都在安全點后開始GC。

2、主動式中斷:不強制中斷線程,只是簡單地設置一個中斷標記,各個線程在執行時主動輪詢這個標記,一旦發現標記被改變(出現中斷標記)時,就將自己中斷掛起。目前所有商用虛擬機全部采用主動式中斷。

安全點既不能太少,以至于 GC 過程等待程序到達安全點的時間過長,也不能太多,以至于 GC 過程帶來的成本過高。安全點的選定基本上是以程序“是否具有讓程序長時間執行的特征”為標準進行選定的,例如方法調用、循環跳轉、異常跳轉等,所以具有這些功能的指令才會產生安全點(在主動式中斷中,輪詢標志的地方和安全點是重合的,所以線程在遇到這些指令時都會去輪詢中斷標志!)。

3.2、安全區域

使用安全點似乎已經完美解決如何進入GC的問題了,但是GC發生的時候,某個線程正在睡覺(sleep),無法響應JVM的中斷請求,這時候線程一旦醒來就會繼續執行了,這會導致引用關系發生變化呢!所以需要安全區域的思路來解決這個問題。線程執行進入安全區域,首先標識自己已經進入安全區域。線程被喚醒離開安全區域時,其需要檢查系統是否已經完成根節點枚舉(或整個GC)。如果已經完成,就繼續執行,否則必須等待,直到收到可以安全離開Safe Region的信號通知!

推薦站點

  • 騰訊騰訊

    騰訊網(www.QQ.com)是中國瀏覽量最大的中文門戶網站,是騰訊公司推出的集新聞信息、互動社區、娛樂產品和基礎服務為一體的大型綜合門戶網站。騰訊網服務于全球華人用戶,致力成為最具傳播力和互動性,權威、主流、時尚的互聯網媒體平臺。通過強大的實時新聞和全面深入的信息資訊服務,為中國數以億計的互聯網用戶提供富有創意的網上新生活。

    www.qq.com
  • 搜狐搜狐

    搜狐網是全球最大的中文門戶網站,為用戶提供24小時不間斷的最新資訊,及搜索、郵件等網絡服務。內容包括全球熱點事件、突發新聞、時事評論、熱播影視劇、體育賽事、行業動態、生活服務信息,以及論壇、博客、微博、我的搜狐等互動空間。

    www.sohu.com
  • 網易網易

    網易是中國領先的互聯網技術公司,為用戶提供免費郵箱、游戲、搜索引擎服務,開設新聞、娛樂、體育等30多個內容頻道,及博客、視頻、論壇等互動交流,網聚人的力量。

    www.163.com
  • 新浪新浪

    新浪網為全球用戶24小時提供全面及時的中文資訊,內容覆蓋國內外突發新聞事件、體壇賽事、娛樂時尚、產業資訊、實用信息等,設有新聞、體育、娛樂、財經、科技、房產、汽車等30多個內容頻道,同時開設博客、視頻、論壇等自由互動交流空間。

    www.sina.com.cn
  • 百度一下百度一下

    百度一下,你就知道

    www.baidu.com
?
最牛一尾中特规律