本文聊聊Java 虛擬機的一些概念奸腺,常見GC算法底層過程,圖解血久,并跟蹤日志做分析突照。篇幅有點長,不要抱著一次看完的心態(tài)去看氧吐,多一點耐心讹蘑。
閱讀建議,先看完《深入理解Java 虛擬機》等相關jvm書籍筑舅,了解java 語言特性
GC base
眾所周知座慰,java虛擬沒有使用引用計數(shù),那么翠拣,使用引用計數(shù)和使用GC root 有何差別角骤?好在哪里?
引用計數(shù)
如php心剥,python等語言,容易造成環(huán)形引用背桐,需要特殊的機制來處理优烧。
Garbage Collection Roots:
- Local variables
- Active threads
- Static fields
- JNI references
JVM GC分為兩步:
標記:Marking
is walking through all reachable objects, starting from GC roots and keeping a ledger in native memory about all such objects
清除:Sweeping
is making sure the memory addresses occupied by non-reachable objects can be reused by the next allocations.
Fragmenting and Compacting
- 寫操作需要花費更多的時間來尋找可用的塊
- 當碎片太多太小,將導致無法為大對象分配內(nèi)存
為了避免內(nèi)存碎片一發(fā)不可收拾链峭,GC的同時JVM也做了內(nèi)存碎片整理畦娄。可以理解為將可達對象全部移到緊挨在一起弊仪,減少碎片熙卡;
Generational Hypothesis
很多對象有以下特點:
- Most of the objects become unused quickly
- The ones that do not usually survive for a (very) long time
所以提出分代的概念:
- Young Generation
- Old Generation(Tenured)
分代并非沒有缺點:
- 不同代之間的對象會互相引用,GC時需要計入實際的GC root.
- JVM由于對 朝生夕死和近乎不死的對象做了優(yōu)化励饵,對于中等壽命的對象表現(xiàn)得很差
Eden
Eden : 對象被創(chuàng)建就默認放Eden驳癌。一般多線程會同時創(chuàng)建多個對象,Eden被切分為更多的Thread Local Allocation Buffer (TLAB for short) 役听。這個buffer允許將大多數(shù)對象分配在線程對應的TLAB里颓鲜,避免了昂貴的線程同步。
- Eden的TLAB不夠分配典予,在共享Eden里分配甜滨,
- 若不足,則觸發(fā)young GC
- youny GC 后還是不夠瘤袖,直接在老年代分配
Survivor Spaces
兩個survivor 有一個是空的
from和to衣摩,eden+from-> to,copy完,from和to的身份要互換
XX:+MaxTenuringThreshold
XX:+MaxTenuringThreshold=0 不復制直接提升到老年代
默認閥值為15
當to裝不下 eden和from所有alive Object的時候?qū)⑦^早觸發(fā)提升捂敌。
Old Generation
由于老年代是預期所有對象傾向于長壽艾扮,只有較少的GC活動既琴,使用移動算法會更劃算。
步驟:
- Mark reachable objects by setting the marked bit next to all objects accessible through GC roots
- Delete all unreachable objects
- Compact the content of old space by copying the live objects contiguously to the beginning of the Old space
PermGen
在Java 8 之前存在栏渺。
永久代呛梆,存放例如classes對象,還有一些內(nèi)置的常量字符串等磕诊。這個區(qū)經(jīng)常導致java.lang.OutOfMemoryError: Permgen space.如沒有明確知道是內(nèi)存泄露填物,解決之道只是簡單把size調(diào)大。
java -XX:MaxPermSize=256m com.mycompany.MyApplication
需要注意的是霎终,jsp 解析會映設為新的java類滞磺,這導致新的class對象產(chǎn)生。還有字節(jié)碼生成cglib等莱褒,是有可能導致PremGen被擠爆的击困。
Metaspace
由于PermGen 的難以使用,Java8 將PermGen 移入Metaspace,并將PermGen 之前一些雜亂的數(shù)據(jù)移到堆上广凸。Metaspace 在本地內(nèi)存分配阅茶,所以不影響java堆的大小。只要進程還有本地內(nèi)存可用就不會觸發(fā)溢出谅海。當Metaspace太大時脸哀,會存入虛擬磁盤,頻繁的磁盤讀寫將導致性能大幅度下降,或者OutOfMemoryError扭吁。
By default, Metaspace size is only limited by the amount of native memory available to the Java process. 但是可以設置大小撞蜂。
java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication
Minor GC
Collecting garbage from the Young space is called Minor GC.
當無法為新對象分配空間時將觸發(fā)GC。例如Eden已經(jīng)滿了侥袜。這意味著蝌诡,頻繁的空間申請將導致頻繁Minor-GC
在整個Minor GC過程中,Tenured(老年代)的概念被忽略枫吧。從老年代到新生代的引用被理解為GC roots,從新生代到老年代的引用在整個標記過程中被忽略浦旱。
Minor GC會觸發(fā)stop-the-world pauses. 當Eden區(qū)的對象大多數(shù)能被識別為垃圾并且從來不copy到Survivor/Old spaces時,這樣的停頓是微不足道的九杂。反之闽寡,大量新生對象不是合格的垃圾,Minor GC將耗費更多的時間尼酿。
tips:
強化作用域概念爷狈,讓對象及時死亡。減少大對象裳擎,長壽對象的使用涎永,短命大對象可以手動賦空指針來提前脫離GC roots;
Minor GC cleans the Young Generation.
Major GC vs Full GC
可以望文生義地理解:
- Major GC is cleaning the Old space.
- Full GC is cleaning the entire Heap – both Young and Old spaces.
可這兩者的概念其實是混雜的。首先羡微,多數(shù)的Major GC 是由Minor GC 觸發(fā)的谷饿,所以分割兩者在很多case下是不可能的。另外妈倔,現(xiàn)在很多垃圾收集器有不同的實現(xiàn)博投。G1收集器使用分塊cleaning,嚴格意義上‘cleaning’這個詞也只能是部分正確。這導致我們沒有必要關注到底是MajorGC還是FullGC盯蝴,我們只需要關注到底是停止了所有應用線程還是GC與應用線程同時運行毅哗。
jstat 打印日志,CMS 為垃圾回收器捧挺,對比GC日志會發(fā)現(xiàn)一次CMS 回收jstat顯示兩次FullGC,而實際上是一次CMS的MajorGC執(zhí)行了兩次stop-the-world
CMS 的過程可以理解為:
- 初始化標記:stop-the-world + stopping all application threads
- 標記和預清理:和應用線程并發(fā)運行
- 最終標記:需要stop-the-world
- 清理: 和應用線程并發(fā)運行
如果我們只是考慮延遲虑绵,那么jstat的結果足夠我們分析,如果是考慮吞吐量闽烙,jstat將誤導我們翅睛。所以GC日志也是需要看的。
Marking Reachable Objects
垃圾回收步驟可大致分為:
- 找出依然存活的對象
- 拋棄其他對象黑竞,也就是死亡的和沒有被使用的
Garbage Collection Roots:
- Local variable and input parameters of the currently executing methods
- Active threads
- Static field of the loaded classes
- JNI references
在標記時捕发,有一些方面需要重點關注:
- 標記的時候需要stop-the-world,當程序stop-the-world,以便于JVM做垃圾回收很魂,叫做save point.
- stop-the-world的停頓時間與堆的大小和整個堆對象數(shù)量無直接關系扎酷,只與堆存活對象數(shù)量相關.所以提高堆大小并不能直接影響標記時間
Removing Unused Objects
移除無用的對象可以分為三類方法:sweep(清除),compacting(壓縮整理)莫换,copy(復制)。
Sweep
Mark and Sweep algorithms use conceptually the simplest approach to garbage by just ignoring such objects. 類似于機械硬盤骤铃,無用的對象并沒有被擦除拉岁,只是被認為是可分配而已。這種方法實現(xiàn)簡單惰爬。
缺點:
- Mark and Sweep 需要維護空閑區(qū)間列表喊暖,以記錄每個空閑區(qū)間的大小和位置,這增加了額外的空間需求撕瞧。
- 容易出現(xiàn)存在大量小空閑區(qū)間碎片陵叽,卻無法找到合適的大空間來分配大對象,導致拋出OutOfMemoryError
Compact
將所有存活對象移動到內(nèi)存的一端
缺點:
- copy對象到內(nèi)存的開頭丛版,維護對象引用巩掺,將增加GC停頓
優(yōu)點:
- 相對于標記清除,標記整理在分配新對象時通過指針碰撞來分配空間页畦,代價非常之低胖替。
- 不存在碎片問題,空閑區(qū)域大小是可見的。
Copy
標記復制独令,和Compact類似端朵,都需要對所有對象維護引用
優(yōu)點:標記和復制過程可以同時進行。
缺點:需要更多的空間來容納存活對象
實際上只需要關注這四種組合燃箭,其他的要么未實現(xiàn)冲呢,要么不切實際。
- Serial GC for both the Young and Old generations
- Parallel GC for both the Young and Old generations
- Parallel New for Young + Concurrent Mark and Sweep (CMS) for the Old Generation
- G1, which encompasses collection of both Young and Old generations
Young | Tenured | JVM options |
---|---|---|
Serial | Serial | -XX:+UseSerialGC |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 | -XX:+UseG1GC |
Serial GC
所有的minor GC都是mark-copy
SerialGC 老年代用mark-sweep-compact招狸,標記整理
java -XX:+UseSerialGC com.mypackages.MyExecutableClass
單線程敬拓,stop-the-world,限制了性能發(fā)揮瓢颅,不適合作為服務器運行恩尾。
Parallel GC
并行GC
老年代使用mark-sweep-compact
-XX:ParallelGCThreads=NNN 設置并行線程數(shù)
java -XX:+UseParallelGC -XX:+UseParallelOldGC com.mypackages.MyExecutableClass
優(yōu)點:高吞吐量:
- during collection, all cores are cleaning the garbage in parallel, resulting in shorter pauses
- between garbage collection cycles neither of the collectors is consuming any resources
弱勢:還是會有延遲,如果是需要低延遲挽懦,這不是好的選擇翰意。
https://plumbr.io/handbook/garbage-collection-algorithms-implementations#serial-gc
為什么full gc會導致年輕代為空?
Concurrent Mark and Sweep
和ParNew搭配
The official name for this collection of garbage collectors is “Mostly Concurrent Mark and Sweep Garbage Collector
”.
It uses the parallel stop-the-world mark-copy algorithm in the Young Generation and the mostly concurrent mark-sweep algorithm in the Old Generation.
年輕代:標記復制
老年代:標記清除
對老年代信柿,它不會造成長時間的停頓:
- 他沒有使用標記復制冀偶,或整理,而是使用free list來管理空閑的區(qū)域渔嚷,這節(jié)省了復制的時間进鸠。
- 它所做的大多數(shù)工作都在標記清除階段,和application并發(fā)運行形病,當然客年,這仍然需要和application競爭cpu資源
設置使用cms
java -XX:+UseConcMarkSweepGC com.mypackages.MyExecutableClass
低延時,可以讓用戶有更好的體驗.因為大多數(shù)時間都存在一部分cpu資源在做gc漠吻,對于吞吐量優(yōu)先的程序量瓜,他的表現(xiàn)不如Parallel GC.
Phase 1: Initial Mark
標記老年代所有和GC roots 或者年輕帶存活對象直接相連的對象,后者至關重要途乃,因為這是分代收集绍傲。
2015-05-26T16:23:07.321-0200: 64.42: [GC (CMS Initial Mark[1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
注:
2015-05-26T16:23:07.321-0200: 64.42: – GC開始的時鐘時間和相對JVM啟動的時間
CMS Initial Mark – 步驟
10812086K – Currently used Old Generation.
(11901376K) – Total available memory in the Old Generation.
10887844K – Currently used heap
(12514816K) – Total available heap
0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] – Duration of the phase, measured also in user, system and real time.
Phase 2: Concurrent Mark
從步驟一標記的對象出發(fā),遍歷整個老年代并標記所有存活對象耍共。
并發(fā)烫饼,不需要stw
需要注意的是,并非所有的存活對象都會被標記出來试读,因為標記的同時杠纵,應用的運行會導致引用關系的改變。
2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-mark1: 035/0.035 secs2] [Times: user=0.07 sys=0.00, real=0.03 secs]3
CMS-concurrent-mark – 在這個階段骇塘,遍歷老年代并標記所有存活對象
035/0.035 secs – 停止時間,顯示經(jīng)過的時間和墻鐘時間(現(xiàn)實世界時間)
[Times: user=0.07 sys=0.00, real=0.03 secs] – 對并發(fā)任務的計時是沒意義的韩容。
Phase 3: Concurrent Preclean.
- 并發(fā)過程
在第二步進行的同時,一些引用關系會被改變款违。當改變發(fā)生時,JVM會將 導致改變的對象對應的堆區(qū)域(Card)標記為“dirty”(臟)群凶。
在pre-cleaning 階段插爹,這些臟對象也被考慮在內(nèi),他們的可達對象也會被標記请梢,標記結束后赠尾,card會被清除。
另外毅弧,一些為Final Remark 過程做的必要準備也將被執(zhí)行
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
CMS-concurrent-preclean – Phase of the collection – “Concurrent Preclean” in this occasion – accounting for references being changed during previous marking phase.
0.016/0.016 secs – elapsed time and wall clock time.
Phase 4: Concurrent Abortable Preclean
并發(fā)可停止的預清理气嫁。不需要stop-the-world
這個過程盡可能把 不需要stop-the-world (Final Remark) 的工作完成。精確測量此階段時間是不可能的够坐,因為它與很多因素有關寸宵,如迭代的次數(shù),過去的墻鐘時間元咙,某些有用的任務被完成梯影。
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-05-26T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean1: 0.167/1.074 secs2] [Times: user=0.20 sys=0.00, real=1.07 secs]3
0.167/1.074 secs – user time(CPU time)/clock time
需要注意的是,user time 比時鐘時間少得多庶香。
It is interesting to note that the user time reported is a lot smaller than clock time.
經(jīng)常我們看到真實時間(clock time)少于user time(cpu time)甲棍,意味著有些工作是并行的,因此消耗的時鐘時間小于CPU時間(cpu time:每個cpu消耗時間的總和)赶掖。這里我們有一小部分的任務需要完成素标,然后GC線程做了很多的等待盾致。
事實上链韭,他們在盡可能地避開長時間的stop-the-world,默認情況下幻梯,這種停頓可能長達5s.
這過程將顯著影響最后標記的stw,并且又很多復雜的配置優(yōu)化和失敗模式呈驶。
Phase 5: Final Remark
最后一次stop-the-world.目標是標記老年代所有存活對象拷泽。前面的Concurrent Preclean是并發(fā)的疫鹊,并不能保證趕得上應用程序?qū)σ玫淖兏俣刃湔埃砸淮伪匾膕top-the-world可以結束這種不確定狀態(tài)。
經(jīng)常的拆吆,CMS盡量在年輕代盡量為空的情況下進行聋迎,以避免兩次stop-the-world接踵而至。(響應時間優(yōu)先)
2015-05-26T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]465.559: [weak refs processing, 0.0000243 secs]65.5595: [class unloading, 0.0013120 secs]65.5606: [scrub string table, 0.0001759 secs7][1 CMS-remark: 10812086K(11901376K)8] 11200006K(12514816K) 9, 0.0110730 secs10] [[Times: user=0.06 sys=0.00, real=0.01 secs]11
CMS Final Remark – “Final Remark” in this occasion – 標記所有老年代的對象枣耀,包括在 前面同步標記過程中創(chuàng)建和修改的引用
YG occupancy: 387920 K (613440 K) – 當前年輕代 已用霉晕、總共的容量
[Rescan (parallel) , 0.0085125 secs] – 重新掃描,在應用停止時并行掃描所有的存活對象,耗時 0.0085125 s牺堰。
weak refs processing, 0.0000243 secs]65.559 – 第一個子過程拄轻,弱引用處理。
class unloading, 0.0013120 secs]65.560 – 第二個子過程伟葫,卸載無用的classes
scrub string table, 0.0001759 secs – 第三個恨搓,最后一個子過程,分別清除 類級別元數(shù)據(jù)和內(nèi)部字符串對應的符號和字符串
10812086K(11901376K) – 此過程結束后筏养,老年代已用和總容量
11200006K(12514816K) – 結束后斧抱,整個堆,已用渐溶,總容量辉浦。
0.0110730 secs – 過程耗時.
[Times: user=0.06 sys=0.00, real=0.01 secs] – Duration of the pause, measured in user, system and real time categories.
至此,所有無用對象都被標記出來了茎辐。
Phase 6: Concurrent Sweep
刪除未使用的對象并回收宪郊,以備后面使用。
2015-05-26T16:23:08.458-0200: 65.56: [CMS-concurrent-sweep-start] 2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep1: 0.027/0.027 secs] [[Times: user=0.03 sys=0.00, real=0.03 secs]
Phase 7: Concurrent Reset
重置內(nèi)部數(shù)據(jù)結構為下次GC做準備荔茬。
優(yōu)點:CMS通過盡可能地將一些工作拆分到并發(fā)線程來減少stop-the-world的 停頓.
缺點:
- 導致老年代內(nèi)存碎片
- 在某些情境下废膘,無法預估GC時間,尤其是遇到大的堆空間慕蔚。
G1 – Garbage First
一個關鍵的設計目標:使得GC時間可預測和可配置丐黄。
G1是一個軟實時的垃圾回收器,你可以設置在x時間內(nèi)stop-the-world不超過y孔飒。它會盡量達標灌闺,但不是一定準確的。
G1 定義了一些新的概念:
- 堆不再切分為老年代和新生代坏瞄,而是分成很多的heap regions(默認2048)桂对。每個區(qū)域可以是Eden,survivor,old region鸠匀。所有Eden和Survivor的集合就是新生代蕉斜,所有old region的集合就是老年代。
這避免了一次性收集整個堆缀棍≌耍可以增量式的收集:每一次只收集區(qū)域集合的一個子集。每次暫停爬范,收集所有的年輕代父腕,并收集部分老年代。
- 在整個并發(fā)過程青瀑,他估算每個region里的實時數(shù)據(jù)量璧亮,并據(jù)此導出收集子集:包含更多垃圾的region應該首先被收集萧诫。這也是他名字 garbage-first collection.
java -XX:+UseG1GC com.mypackages.MyExecutableClass
Evacuation Pause: Fully Young
一開始,缺乏并發(fā)過程的相關信息枝嘶,所以以完全年輕的狀態(tài)運行帘饶。當年輕代被填補,應用線程停止,年輕代存活的數(shù)據(jù)被copy到survivor群扶,部分空閑區(qū)域變成survivor尖奔。
copy的過程稱為“Evacuation”(疏散),類似于其他ygc的copy穷当。
0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs]
[Parallel Time: 13.9 ms, GC Workers: 8]
…
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.4 ms]
…
[Eden: 24.0M(24.0M)->0.0B(13.0M) Survivors: 0.0B->3072.0K Heap: 24.0M(256.0M)->21.9M(256.0M)]
[Times: user=0.04 sys=0.04, real=0.02 secs]
0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs] – G1 停止提茁,只清理年輕代,開始于 JVM 啟動后134ms 馁菜,耗費的墻鐘時間為0.0144s.
[Parallel Time: 13.9 ms, GC Workers: 8] – 耗時 13.9 ms (real time) 茴扁,并行使用 8 個線程
… – 略
[Code Root Fixup: 0.0 ms] – 釋放掉那些用來管理并行活動的數(shù)據(jù)結構,需要經(jīng)常接近于0.這是有序進行的
[Code Root Purge: 0.0 ms] – 清除更多的數(shù)據(jù)結構汪疮,應該很快峭火,不一定幾乎為零。有序智嚷。
[Other: 0.4 ms] – 繁雜的其他任務卖丸,很多也是并行的
… – 略
[Eden: 24.0M(24.0M)->0.0B(13.0M) – 暫停前后Eden區(qū)的已使用(總容量)
Survivors: 0.0B->3072.0K – Space used by Survivor regions before and after the pause
Heap: 24.0M(256.0M)->21.9M(256.0M)] – Total heap usage and capacity before and after the pause.
[Times: user=0.04 sys=0.04, real=0.02 secs] – Duration of the GC event, measured in different categories:
user – 所有GC線程耗費CPU時間的總和
sys – 系統(tǒng)調(diào)用和系統(tǒng)等待所耗費的時間
real – 應用停止的墻鐘時間。
G1 的理論還是沒搞懂盏道,暫時不寫下去了稍浆。