Java 垃圾回收詳解
知道 Java 的垃圾回收(GC)怎麼工作有什麼好處?作為一個軟體工程師,滿足智力上的好奇心可能是一個理由,但是同時理解 GC 怎麼工作可以幫助你寫出更好的 Java 應用。
這是我自己非常個人、主觀的看法,但是我相信一個精通 GC 的人很可能是一個更好的 Java 開發者。如果你對 GC 的過程感興趣,那說明你已經有了開發一定規模應用的經驗。如果你曾仔細思考選擇正確的 GC 演算法,說明你已經完全理解了你所開發的應用的功能。當然,這可能不是評判一個優秀開發者的通用標準。然而,當我說要想成為一名出色的 Java 開發者必須理解 GC 時,我想很少會有人反對。
這是我「成為 Java GC 專家」 系列文章的第一篇。本文將介紹 GC,在下一篇文章中,我將討論分析 GC 的狀態以及 NHN 的 GC 調節示例。
在了解 GC 之前你需要知道一個術語。這個術語就是「全局暫停事件」(stop-the-world)。不管你選擇什麼 GC 演算法,全局暫停事件都會發生。全局暫停事件意味著JVM將停止當前應用的運行來執行 GC。當全局暫停事件發生時,除了 GC 所需要的線程外,所有的線程都會停止執行任務。被中斷的任務只有當 GC 任務完成以後才會恢復。GC 調節通常意味著減少全局暫停事件的次數。
垃圾回收的來源
Java 不會在代碼中手動指定一塊內存再釋放它。有的開發者會將相關對象置為 null 或者使用 System.gc() 方法手動釋放內存。設置為 null 不是什麼大問題,但是調用 System.gc() 方法會劇烈的影響系統的性能,所以不應該使用。(幸好,我還沒有看到 NHN 的開發者有使用這個方法。)
在 Java 中,由於開發者不需要在代碼中手動釋放內存,垃圾搜集器會查找不需要的對象(垃圾)並釋放它們。垃圾搜集器基於以下兩條假設創建(稱它們為推測或者先決條件也許更準確)
大多數對象很快變成不可達。
只存在少量從老的對象到新對象的引用
這些假設稱為「弱分代假設」(weak generational hypothesis),為了強化這一假設,HotSpot 虛擬機在物理上分為兩個部分-新生代(young generation) 和 老年代(old generation)。
新生代:大多數新創建的對象都存放在這裡。因為大多數對象很快就會變得不可達,很多對象都在新生代創建,然後就消失。當一個對象從這個區域消失的時候,我們就說發生了一次「小的 GC」(minor GC)。
老年代:那些在新生代存活下來,並沒有變成不可達的對象被複制到這裡。它通常要比新生代大。由於容量更大,GC 發生的次數就沒有新生代頻繁。當對象從老年代消失時,我們就說發生了一次「大 GC」(major GC)(或者是 "全 GC"(full GC))。
我們一起來看一下這幅圖:
圖1:GC 區域和 數據流程
上圖中的持久代(permanent generation)通常也稱為「方法區(method area)」,它用於存儲類或者字元常量。所以這個區域不是用於永久存儲從老年代存活下來的對象。這個區域也可能會發生 GC。這個區域發生的 GC 也算作大 GC。
有人可能會想:
如果一個處於老年代的對象需要引用一個處於新生代的對象會怎麼樣?
為了解決這個問題,在老年代有一個稱為"card table"的東西,是一個512位元組大小的塊。當老年代中的對象要引用一個新生代的對象時,它就會被記錄在這個 table 中。當新生代執行 GC 的時候,只需要搜索這個 table 來確定它是否屬於需要 GC 的對象,而不用檢查老年代所有引用的對象。card table 通過 write barrier 管理。write barrier 給小 GC 性能上帶來極大的提升。儘管會有一點額外的開銷,但是 GC 的總體時間減少了。
圖2:Card Table 的結構
新生代的組成
為了理解 GC, 我們先了解一下新生代,也就是對象第一次被創建的地方。新生代被分成3個區域。
一個 Eden 區
兩個 存活(Survivor) 區
總共3個區域,其中兩個是存活區。每一個區域的執行順序是這樣的:
1、大部分新創建的對象都處於 Eden 區
2、在 Eden 區域執行第一次 GC 以後,存活下來的對象被移動到其中一個存活區。
3、在 Eden 區域再次執行 GC 以後,存活下來的對象繼續堆積已經有對象的那個存活區。
4、一旦一個存活區被存滿,存活對象就會被移動到另一個存活區。然後被存滿的那一個存活區數據就會被清掉(修改為無數據狀態)。
5、如此反覆一定次數之後,還處於存活狀態的對象被移動到老年區。
如果你仔細檢查這些步驟,存活區域總是有一個是空的。如果兩個存活區域同時都有數據,或者同時都為空,這意味著你的系統存在問題。
通過小 GC 將數據堆積到老年代的過程可以參考下圖:
圖3:GC 前後
注意在 HotSpot 虛擬機中,有兩種技術用於快速內存分配。一個成為「bump-the-pointer」,另一個稱為「TLABs(Thread-Local Allocation Buffers)」。
Bump-the-pointer 技術跟蹤 Eden 區域最後分配的對象。那個對象將處於 Eden 區域的頂部。如果有新的對象需要創建,只需要檢查對象的大小是否適合 Eden 區域。如果合適,新的對象將被放在 Eden 區域,並且新的對象處於頂部。所以,當創建新的對象時,只需要檢查上一次創建的對象,這樣可以做到較快的內存分配。但是,如果是在多線程環境那將是另外一個場景。為了保證 Eden 區域多線程使用的對象是線程安全的,將不可避免的使用鎖,這會導致性能的下降。HotSpot 虛擬機使用 TLABs 來解決這個問題。使用 TLABs 允許每一個線程在 Eden 區域有自己的一小塊分區。由於每一個線程只能訪問它們自己的 TLAB,即使是 bump-the-pointer 技術也可以不使用鎖就分配內存。
到現在我們快速的概述了新生代的 GC。你不必完全記住我剛才所提到的兩種技術。你不知道它們也沒什麼大不了。但是請記住:對象是在 Eden 區域創建,然後長期存活的對象通過存活區移動到老年代。
老年代的 GC
老年代在數據存滿時會執行 GC。各種 GC 的執行過程因類型而異,所以如果你知道不同類型的 GC, 理解起來會容易一些。
在 JDK 7中,一共有5中類型的 GC。
1、Serial GC
2、Parallel GC
3、Parallel Old GC(Parallel Compacting GC)
4、ConCurrent Mark & Sweep GC (CMS)
5、Garbage First(G1)GC
所有這些 GC 當中,serial GC 不可以在服務端使用。這種 GC 在只有一個 CPU 的桌面系統中才會創建。使用 serial GC 會明顯的降低應用的性能。
現在我們一起來學習每一種 GC。
Serial GC(-XX:+UseSerialGC)
上一段中我們介紹的新生代的 GC 使用的是這種類型。老年代的 GC 使用叫做 "標記-清除-壓縮(mark-sweep-compact)"的演算法。
1、這個演算法的第一步是標記老年代中的存活對象
2、然後、從頭開始檢查堆,將存活的對象放到後面(交換)
3、最後一步,用存活對象從頭開始填充堆,這樣這些存活對象連續堆放,並且將對分為兩部分:一部分有對象另一部分沒有對象(壓縮)
Serial GC 適合小型內存和有少量CPU 內核的環境。
Parallel GC(-XX:+UseParallelGC)
圖4:Serial GC 和 Parallel GC 之間的差別
從這張圖片上很容易發現Serial GC 和 Parallel GC 之間的差異。Serial GC 只是用一個線程執行 GC,parallel GC 使用多個線程執行 GC,所以更快。當內存足夠並且 CPU 內核夠多時這種 GC 非常有用。它也被稱作」吞吐量 GC(throughput GC)。「
Parallel Old GC(-XX:+UseParallelOldGC)
JDK 5 以後開始支持 Parallel Old GC。與並行 GC 相比,唯一的區別是這個 GC 演算法是為老年代設計的。它的執行一共有三個步驟:標記-匯總-壓縮。匯總這一步為 GC 已經執行過的區域單獨標記存活的對象,這一步和 標記-交換-壓縮 演算法中的交換步驟是不一樣的。這需要通過更複雜的步驟來完成。
CMS GC(-XX:UseConcMarkSweepGC)
圖5:串列 GC 和 CMS GC
如你所見,CMS GC 比我們前面所介紹的任何 GC 都要複雜的多。剛開始的 初始標記步驟很簡單。離類載入器最近的對象中的存活對象被搜索出來。所以,暫停時間很短。在並發標記步驟中,剛才已經確認的存活對象所引用的對象被跟蹤並檢查。這一步的差別在於它在處理的同時其他線程同時也在處理。在重新標記階段,新添加的對象或者在並發標記階段被停止引用的對象會被檢查。最後,並發清除階段,垃圾回收過程被執行。垃圾回收在其他線程還在進行的時候就執行。因為這一類型的 GC 是以這樣的方式執行,GC 的暫停時間很短。CMS GC也被稱作低延時 GC,所以當響應時間對所有的應用都很關鍵的時候使用這種 GC。
CMS GC 擁有較短的全局暫停時間這一優點,同時也有以下缺點。
它比其他類型的 GC 使用更多的內存和 CPU
默認沒有提供壓縮演算法。
在使用這種 GC 之前需要認真檢查。同時,如果多個內存碎片需要壓縮,全局暫停時間的時間會比任何其他類型的 GC 都要長。所以你需要確認壓縮任務執行的頻率和時間。
G1 GC
最後,我們一起來看一下垃圾優先(G1)GC。
圖6:G1 GC 的布局
如果你想理解 G1 GC,忘掉你所知道的新生代和老年代的所有一切。如上圖所示,每一個對象被分配到每個網格中,然後會執行 GC。一旦一個區域被填滿,對象就會被分配到另一個區域,然後執行一次 GC。在G1 GC 中,將數據從新生代的3個區域移動到老年區的所有步驟都不存在。G1 GC 的創建時用於替換 CMS GC,因為從長遠看後者會引發很多問題。
G1 GC 最大的優點是性能。它比我們前面討論過的任何 GC 類型都要快。但是在 JDK 6中,這是一個所謂的早期版本所有隻能用於測試。JDK 7的官方版本中已經包含這一類型 GC。以我個人的意見,我們在將 JDK 7應用到 NHN 的實際服務之前需要很長的時間的測試(至少一年),所以你可能需要等待一段時間。同時我聽說了幾次在 JDK 中使用 G1 GC 後JVM出現崩潰。所以請繼續等待直到它更穩定。


TAG:青峰科技 |
※async/await使用深入詳解
※stringr包詳解
※HashMap詳解
※詳解ADI收購Linear
※詳解TogetherJS
※實用!加拿大Super Visa申辦條件及資料詳解
※詳解Siamese網路
※Air Jordan 11低幫改裝,鞋底solo氣墊改zoom詳解圖
※.gitignore詳解及編寫
※乾貨:詳解 Tomcat 配置文件 server.xml
※Windows窗體數據抓取詳解
※MyBatis配置文件詳解
※privalia是什麼平台?privalia平台介紹、賣家開店入駐條件、收費標準詳解
※Tensorboard 詳解
※eBay運營之best match規則詳解
※Summary 數據類型詳解
※MyBatis 配置 typeHandlers 詳解
※Linux wget 命令用法詳解
※Python super 詳解
※詳解 RestTemplate 操作