JVM GC 總結。
周志明大大的《深入理解Java虛擬機》出第三版了,早早的買了這本書,卻一直沒有花時間看。近來抽空溫習了一下,感覺又有了新的收穫。這裏簡單總結下。
GC的由來
由於堆的動態性,操作系統將堆交由給了開發者自己管理,手動申請,手動釋放。對於C++
,則是將這個權限繼續交給了開發者,而對於Java
,則是將這個過程自動化了。為什麼要釋放內存呢?最簡單的原因就是操作系統一共給你了4G的內存空間,你需要的時候,就去借用。有借有還,再借不難,只借不還,最後4G內存空間被用完了,你就無法再申請新的內存了。內存泄漏,就是只借不還。
JVM
在操作系統與開發者之間又封裝了一層,間接的接管了內存的劃分。同時也將堆統一管理起來,使得開發者只管借用內存,由JVM
負責回收,了解JVM
的回收機制,明白它的原理,能讓開發者在不同的場景下,定製不同的回收規則,提高回收效率。
關於GC的思考
如果讓我設計一個能自動回收垃圾的虛擬機,我會怎麼設計呢?
-
什麼時候開始回收?
-
怎麼判斷這部分內存可以回收?
-
怎麼回收這部分的垃圾?
這3個問題,也是JVM
開發者一直在思考的問題。之前簡單了解過JVM
,就知道JVM
會有Stop The World
的問題,這對於用戶體驗來說非常不好,其根本原因便是因為在回收垃圾的時候,用戶線程可能會修改這部分內存,如果不暫停用戶線程,則可能會導致嚴重的問題,而如何減少Stop The World
的時候,甚至讓其消失,是各個垃圾回收器一直追求的目標。
哪些內存可以回收?
對於一個對象來說,當不存在任何一個引用能夠訪問到這個對象的時候,則說明這個對象可以進行回收。因為沒有任何引用指向這個對象,那麼這個對象就不能被讀或寫。
-
引用計數法
前面說判斷一個對象可以被回收的標準就是是否還有引用指向這個對象,所以最容易想到的便是引用計數法,通過判斷一個對象的引用數量即可,可是這樣無法判斷兩個循環引用的對象。
-
可達性分析
可達性分析指的是從目前程序中正在使用的所有引用的對象出發,循環遍歷所有能找到的對象。
作為出發的點的這些對象,被稱為
GC Roots
GC Roots
主要包括以下幾種:-
在虛擬機棧(比如棧幀中的本地遍歷表)中引用的對象
-
靜態屬性引用的對象
-
常量池引用對象(比如
String Table
)
-
本地方法棧引用的對象
-
Java
虛擬機內部的引用對應的對象
-
所有被同步鎖持有的對象
-
…
總體來說,就是當前程序中正在被使用的引用所指向的對象會被作為
GC Roots
-
從GC Roots
出發,依次查找,就能標記出當前存活的對象。但是標記這個過程,細節上依然存在問題:
-
STW
: 標記是通過引用查找對象的,如果在標記過程中,用戶修改了引用的對象,那麼會導致不可預估的後果,因此一般標記過程中,是會STW
的 -
跨代標記 : 現在的垃圾回收器,大多數都是分代,或者分區域回收的,也就是說,可能進行垃圾回收的時候,不是標記所有的垃圾,而是標記一部分,比如老年代或者新生代。此時就存在一個問題,跨代引用。比如一個新生代的對象,僅僅被一個老年代對象引用的話,對於
Yong GC
來說,是不會掃描老年代對象的,這個時候就會造成誤判。解決這個誤判的方法便是記憶集(Remembered Set),記憶集通過AOP
技術生成寫屏障來維護。前面說了從
GC Roots
開始掃面,那分代收集的,怎麼知道哪些對象是新生代的,哪些對象是老年代的呢?因為GC Roots
是包含了所有引用的。後面想想,其實對象的分代信息是存放在對象頭裡面的。在掃描GC Roots
的時候,只保留新生代的對象即可。這樣基本能保證掃描到的是新生代對象,然後老年代對新生代引用交給記憶集實現就行(自己的猜測,沒有證據)JVM
書中說道通過AOP
生成的寫屏障會使得只要有更新操作,無論更新的是不是老年代對新生代對象的引用,都會使卡表變髒,不過這樣的代價相對來說是能接受的。 -
GC Roots 需要掃描的引用過多 :隨着現在
Java
應用越做越大,Java
堆也越來越大,GC Roots
的掃描是需要STW
的,如果每次GC
都逐個掃描,會非常的浪費時間。解決這個問題的辦法就是OopMap
,使用OopMap
記錄應用程序所存放的引用,每次需要GC
的時候掃描這個OopMap
即可生成對應的GC Roots
,OopMap
通過安全點和安全區域來維護,只有在安全點或安全區域的時候,才更新OopMap
和進行垃圾回收。 -
併發標記過程可能丟失存活的對象 :從
CMS
到G1
,都將從GC Roots
出發標記存活對象的過程修改成併發的,這樣會需要解決的問題就是標記過程中如果用戶修改了對象的引用,可能會導致本應該存活的對象”丟失“(可以通過三色標記分析),相應的解決方案便是破壞存活對象消失的必要條件,分別是增量更新(Incremental Upate
)和原始快照(Snapshot At The Begin
,SATB),增量更新破壞的是第一個條件,每插入一個引用,就都記錄下來,而原始快照破壞的是第二個條件,每刪除一個,都將其記錄下來。增量更新和併發快照也是通過前面所說的
AOP
技術生成寫屏障來維護
通過以上分析以及解決方案,基本明白了怎麼標記那些內存可以回收,接下來需要分析的就是什麼時候開始回收
什麼時候開始內存回收?
對於內存回收來說,開始也需要有一定的講究,理論上來說,隨時隨地都可以開始內存回收,但是如果回收時使用的內存過多,會導致GC
時間過程,進而STW
時間也會很長,如果回收過於頻繁,又會導致吞吐量下降,畢竟每次掃描GC Roots
都回STW
的。
同時,前面還說過,對於用戶線程來說,需要將用戶線程運行到安全點,更新對應的OopMap,才能開始垃圾回收。
因此,對應何時GC
,有以下幾點分析:
-
對於新生代來說,一般新生代滿了(
Eden + Survivor1
)就會開始進行(Yong/Minor GC
) -
對於老年代來說,一般是老年代滿了了會開始
Full/Major GC
注意:這裏的滿了,需要根據具體的回收器不同,來衡量真正的滿,對於沒有併發過程的
GC
,老年代滿一般指的是真正到達100%,已經無法分配內存了,對於有併發過程的GC
,則需要預留出來空間給用戶線程在併發過程中同時申請內存,如果預留內存過小,則會使用非併發垃圾回收器進行Full GC
CMS
:-XX:CMSInitiatingOccupancyFraction
設置,默認92%
(JDK 8),表示當老年代垃圾佔用到92%
就開始老年代回收,JDK 9
后便無法使用CMS
G1
:-XX:G1ReservePercent
設置,默認為10
,表示當整個Java
堆使用到達90%
,就開始回收。同時配合的參數還有-XX:InitiatingHeapOccupancyPercent=n
,默認值為45
,表示使用率到達45%
就啟動標記周期。這裏的GC
是Mixed GC
一般來說,只有
CMS
才有Major GC
,其他老年代GC
都會回收整個Java
堆,也稱為Full GC
-
統計得到的
Minor GC
晉陞到老年代的平均大小大於老年代剩餘的空間。(JDK 6 之後已經刪除了擔保規則) -
GC
併發失敗(concurrent mode failure
): 情況如前面說的,併發標記過程中,又出現了新生代晉陞的情況,但是此時老年代剩下的內存不足夠放下晉陞的對象的時候,會生導致Full GC
這裏的
Full GC
和情況1中說到達預留空間的GC
不一樣,情況1是正常進行的GC
,而這個併發失敗卻是GC
過程中出現了異常,一般需要切換到非併發GC
,此時性能會大大下降 -
方法區區域被使用完畢:
JDK 8
之後將方法區從Perm Gen
替換成了元空間,一般來說元空間大小理論上等於本地內存大小,不過元空間有一個默認初始值,到達默認初始值后,會通過Full GC
擴大注意:
G1
只有Yong GC
和Mixed GC
。沒有Full GC
的概念,也就是說如果需要回收方法區的話,只能退化為Serial GC
進行Full GC
CMS
可以通過-XX:+CMSClassUnloadingEnabled
設置併發回收方法區 -
最大連續空間裝不下大對象:對於
CMS
,基於標記-清除算法來說,即使空間足夠,但是由於內存碎片,裝不下分配的大對象時,會進行一次Full GC
,對於G1
來說,當分配巨型對象的時候,如果在老年代無法找到連續的Humongous
的時候,會進行Full GC
-
用戶執行
System.gc()
,可以通過-XX:+DisableExplicitGC
屏蔽 -
…
怎麼回收這些內存
最後一步便是怎麼回收這些內存。怎麼回收,書中介紹不多,總體來說有以下三種:
-
標記-清除(
Mark-Sweep
):最原始的方法,實現簡單,不用移動對象,很容易做到不用
Stop The Word
,但是缺點也很致命,容易產生內存碎片。標記清除的速度一般,
Mark
階段與活對象的數量成正比,
Sweep
階段與整堆大小成正比。目前只有
CMS
使用這種回收方案
-
標記-複製(
Mark-Copying
):基於標記-清除修改的垃圾回收算法,需要移動對象。 前期標記,然後複製活下來的對象到另一個區域,再總體回收整塊區域。標記複製算法對於新生代這種專門放朝生夕死的對象效率非常高,因為存活下來的對象少,所以
Mark
階段和
Copying
階段花費的時間都會比較少,幾乎所有的分代
GC
新生代都是使用的這種算法
-
標記-整理(
Mark-Compact
): 基於標記-清除算法修改的垃圾回收器,需要移動對象。前期標記,然後將所有對象移動到一起,再對剩餘的區域進行回收,速度最慢,但是不會產生內存碎片。
對於新生代使用標記-複製算法,是毋庸置疑的。但是對於老年代,使用標記清除還是標記整理,需要有一定的考量。因為使用標記-清除,不用移動對象,速度會相對來說比較快,但是由於存在內存碎片,無法使用指針碰撞的方式分配內存,而不得不使用“分區空閑分配鏈表”來解決內存分配的問題,這樣會對在內存分配帶來一定的效率影響,而標記-整理算法需要移動對象,特別是對於老年代這種大對象來說,移動這些對象將是一種極為負重的操作,但是標記-整理不會產生內存碎片。
因此,基於以上考慮,對於CMS
這種側重響應速度,致力於減少STW
時間的回收器來說,選擇了標記-清除算法,但是由於內存分配是一個非常頻繁的操作,使用”分區空閑分配鏈表”會降低整個垃圾回收器的吞吐量,因此,對於Parllel Scavenge
這種注重回收吞吐的垃圾回收器來說,選擇了標記-整理算法。當然,對於G1
則是吞吐和響應速度都比較注重,權衡之下,選擇了標記-整理(全局)算法。
GC
的概念,到這裏基本總結完畢,但是,如果僅僅是理論,只是讓我們記着一些概念性的東西,接下來,我會結合CMS
,G1
的GC
日誌以及《深入理解JVM》第四章的內容,聊一聊如何分析以及查看GC
過程,簡單介紹如果進行GC
調優。
個人公眾號:
不定期更新一些經典Java書籍總結。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※Google地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計最專業,超強功能平台可客製化