對于垃圾回收霞幅,對于Java程序員來說應(yīng)該是不陌生的娩脾。想要長遠(yuǎn)的發(fā)展赵誓,必須對這塊機制有所了解,
這樣才能寫出更高效的代碼。如果遇到性能的瓶頸俩功,那么肯定要從這方面去分析幻枉,做一些調(diào)優(yōu)。
前言
垃圾回收诡蜓,在Java虛擬機中熬甫,垃圾指的是死亡對象所占用的空間,顧明思議就是對內(nèi)存中已死的對象進(jìn)行回收蔓罚,那么如何找出已死的對象椿肩,如何進(jìn)行回收,已近新的對象內(nèi)存如何分配豺谈,
就是垃圾回收要考慮的地方郑象。
如何判斷對象已死
所謂的對象已死,就是在內(nèi)存中游離的對象茬末,沒有再被用到厂榛,但有占用空間,目前主要有兩種方法去判斷丽惭。
哪些對象是需要回收的呢
猿們都知道JVM的內(nèi)存結(jié)構(gòu)包括五大區(qū)域:程序計數(shù)器击奶、虛擬機棧、本地方法棧责掏、堆區(qū)柜砾、方法區(qū)。
其中程序計數(shù)器换衬、虛擬機棧局义、本地方法棧3個區(qū)域隨線程而生、隨線程而滅冗疮,因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性萄唇,
就不需要過多考慮回收的問題,因為方法結(jié)束或者線程結(jié)束時术幔,內(nèi)存自然就跟隨著回收了另萤。而Java堆區(qū)和方法區(qū)則不一樣,
這部分內(nèi)存的分配和回收是動態(tài)的诅挑,正是垃圾收集器所需關(guān)注的部分四敞。
垃圾收集器在對堆區(qū)和方法區(qū)進(jìn)行回收前,首先要確定這些區(qū)域的對象哪些可以被回收拔妥,
哪些暫時還不能回收忿危,這就要用到判斷對象是否存活的算法!
引用計數(shù)法
這個做法是為每個對象添加一個引用計數(shù)器没龙,用來統(tǒng)計指向該對象的引用個數(shù)铺厨,每當(dāng)有一個地方引用它時缎玫,計數(shù)器+1,當(dāng)失效后會減1解滓。
一旦該對象的引用計數(shù)器為0赃磨,則說明該對象已死亡,便可以被回收洼裤。
這種方法會帶來一個問題:除了需要格外的空間來存儲引用計數(shù)器邻辉,以及繁瑣的更新操作,還有一個重大漏洞腮鞍,那就是無法處理循環(huán)引用
問題值骇。下面這兩個對象是不會被標(biāo)記會死亡的:
可達(dá)性分析
目前Java虛擬機的主流垃圾回收器采取的可達(dá)性分析算法。這個算法的實質(zhì)在于將一系列GC Roots作為初始的存活對象集合(live Set)移国,然后從該集合出發(fā)雷客,
探索所有能夠被該集合引用的對象,并加入到該集合中桥狡,這個過程就是標(biāo)記過程搅裙。最終,未被探索到的對象便是死亡裹芝,是可回收的部逮。
那么什么是GC Roots,其實就是由堆外指向堆內(nèi)的嫂易,一般GC Roots包括一下幾種:
- 1:Java方法棧中的局部變量兄朋。
- 2:已加載類的靜態(tài)變量。
- 3:JNI中引用對象怜械。
- 4:已啟動但為停止的Java線程
雖然可達(dá)性分析颅和,可以解決循環(huán)引用問題,但自身也存在一些問題缕允,比如說峡扩,在多線程下,其他線程可能會更新已經(jīng)訪問過的對象中的引用障本,
從而造成誤報(將引用設(shè)置為null)或者漏報(將引用設(shè)置為未被訪問過的對象)教届。
垃圾收集算法
其實垃圾回收目前就三種方式,清除驾霜、壓縮與復(fù)制案训,下面我們分別來看下這幾種算法的過程。
標(biāo)記-清除
根據(jù)名字粪糙,就知道强霎,這個算法是分為兩步的,先標(biāo)記需要回收的對象蓉冈,很明顯城舞,這個算法經(jīng)過多次收集后轩触,回出現(xiàn)很多內(nèi)存碎片,之后可能對于大對象無法存放椿争。
復(fù)制算法
復(fù)制算法的提出是為了克服句柄的開銷和解決內(nèi)存碎片的問題。它將可用內(nèi)存按容量劃分為大小相等的兩塊熟嫩,每次只使用其中的一塊秦踪。當(dāng)這一塊的內(nèi)存用完了,
就將還存活著的對象復(fù)制到另外一塊上面掸茅,然后再把已使用過的內(nèi)存空間一次清理掉椅邓。
標(biāo)記-整理算法
標(biāo)記-整理算法采用標(biāo)記-清除算法一樣的方式進(jìn)行對象的標(biāo)記,但在清除時不同昧狮,在回收不存活的對象占用的空間后景馁,會將所有的存活對象往左端空閑空間移動,
并更新對應(yīng)的指針逗鸣。標(biāo)記-整理算法是在標(biāo)記-清除算法的基礎(chǔ)上合住,又進(jìn)行了對象的移動,因此成本更高撒璧,但是卻解決了內(nèi)存碎片的問題透葛。
分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據(jù)對象存活的生命周期將內(nèi)存劃分為若干個不同的區(qū)域卿樱。
一般情況下將堆區(qū)劃分為老年代(Tenured Generation)和新生代(Young Generation)僚害,在堆區(qū)之外還有一個代就是永久代(Permanet Generation)。老年代的特點是每次垃圾收集時只有少量對象需要被回收繁调,
而新生代的特點是每次垃圾回收時都有大量的對象需要被回收萨蚕,那么就可以根據(jù)不同代的特點采取最適合的收集算法。
幾種算法的比較
算法 | 優(yōu)點 | 缺點 |
---|---|---|
標(biāo)記-清除 | 簡單 | 1. 標(biāo)記和清除兩個過程的效率都不高蹄胰;2. 標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片 |
復(fù)制 | 實現(xiàn)簡單岳遥,運行高效 | 1. 減少了內(nèi)存使用空間;2. 在對象存活率較高時需要進(jìn)行較多的復(fù)制操作(不適合老年代) |
標(biāo)記-整理 | 根據(jù)老年代的特點提出的一種算法裕寨,適合老年代 | 只適合于某些特定情況 |
分代收集 | 使用多種收集算法寒随,根據(jù)各自的特點選用不同的收集算法 | 具體實現(xiàn)過程比較復(fù)制 |
安全點 (Safe Point)
HotSpot 并沒有為每條指令都生成 OopMap,而只是在 “特定的位置” 記錄了這些信息帮坚,這些位置稱為安全點(Safepoint)妻往,
即程序執(zhí)行時并非在所有地方都能停頓下來開始 GC,只有在達(dá)到安全點時才能暫停试和。
Safepoint 的選定既不能太少以至于讓 GC 等待時間太長讯泣,也不能多余頻繁以至于過分增大運行時的負(fù)載。所以阅悍,
安全點的選定基本上是以 “是否具有讓程序長時間執(zhí)行的特征” 為標(biāo)準(zhǔn)進(jìn)行選定的——因為每條指令執(zhí)行的時間非常短暫好渠,
程序不太可能因為指令流長度太長這個原因而過長時間運行昨稼,”長時間執(zhí)行” 的最明顯特征就是指令序列復(fù)用,例如方法調(diào)用拳锚、循環(huán)跳轉(zhuǎn)假栓、異常跳轉(zhuǎn)等,
所以具有這些功能的指令才會產(chǎn)生 Safepoint霍掺。
Stop-The-World
在可達(dá)性分析的時候匾荆,如果有其他工作線程在執(zhí)行或者結(jié)束,那么就會產(chǎn)生新的垃圾杆烁,這就處于一個邊收集便產(chǎn)生的情況牙丽。所以可達(dá)性分析必須
在一個能確保一致性的快照中執(zhí)行。傳統(tǒng)的垃圾回收算法采用的是一種簡單粗暴的方式兔魂,那便是Stop-The-World烤芦,停止其他非垃圾回收線程的工作,
直到完成垃圾回收析校,這也就造成了所謂的暫停時間(GC Pause)构罗。
Java虛擬機中的Stop-The-World是通過安全點(Safepoint)機制來實現(xiàn)的,但java虛擬機收到Stop-The-World請求智玻,它便會等待所有線程都到達(dá)
安全點绰播,才允許請求Stop-The-World的線程進(jìn)行獨占的工作。TW是存在所有垃圾收集器中的尚困,即使是CMS和G1這種幾乎不停頓的垃圾收集器中蠢箩,
枚舉根節(jié)點(GC Root)時也是必須要停頓的。
垃圾收集器
收集器可以從兩個方面進(jìn)行分類:- 1:單線程或者多線程事甜。
- 2:收集對象類型谬泌。
下面看下收集器圖,其中上面區(qū)域表示收集的是年輕代逻谦,下面區(qū)域收集的是老年代掌实,當(dāng)然G1收集器是都可以的。
線條連接的兩個收集器邦马,說明可以組合一起使用贱鼻。
Serial 收集器(復(fù)制算法)
新生代單線程收集器,標(biāo)記和清理都是單線程滋将,優(yōu)點是簡單高效邻悬。它只會是使用一個 CPU 或一條收集線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時随闽,必須暫停其他所有的工作線程父丰,直到它收集結(jié)束
是client級別默認(rèn)的GC方式,可以通過-XX:+UseSerialGC來強制指定掘宪。
Serial Old 收集器(標(biāo)記-整理算法)
Serial Old 是 Serial 收集器的老年代版本蛾扇,它同樣是一個單線程收集器攘烛,使用 “標(biāo)記-整理” 算法。Serial/Serial old 收集器的運行過程如圖
ParNew收集器(停止-復(fù)制算法)
新生代收集器镀首,可以認(rèn)為是Serial收集器的多線程版本,在多核CPU環(huán)境下有著比Serial更好的表現(xiàn)坟漱。
由于存在線程交互的開銷,該收集器在通過超線程技術(shù)實現(xiàn)的兩個 CPU 的環(huán)境中都不能百分之百地保證可以超越 Serial 收集器更哄。但是芋齿,當(dāng) CPU 的數(shù)量增加時,
它對于 GC 時系統(tǒng)資源的有效利用還是很有好處的竖瘾,
它默認(rèn)開啟的收集線程數(shù)與 CPU 的數(shù)量相同沟突,在 CPU 非常多(使用超線程時)的環(huán)境下花颗,可以使用 -XX:ParallelGCThreads 參數(shù)來限制垃圾收集的線程數(shù)捕传。
ParNew/Serial old 收集器的運行過程如下圖所示:
Parallel Scavenge收集器(停止-復(fù)制算法)
并行收集器,追求高吞吐量扩劝,高效利用CPU庸论。吞吐量一般為99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC線程時間)棒呛。適合后臺應(yīng)用等對交互相應(yīng)要求不高的場景聂示。是server級別默認(rèn)采用的GC方式,
可用-XX:+UseParallelGC來強制指定簇秒,用-XX:ParallelGCThreads=4來指定線程數(shù)鱼喉。
Parallel Old收集器(停止-復(fù)制算法)
Parallel Scavenge收集器的老年代版本,并行收集器趋观,吞吐量優(yōu)先扛禽。
CMS(Concurrent Mark Sweep)收集器(標(biāo)記-清理算法)
整個過程分為4個步驟:1. 初始標(biāo)記(CMS initial mark) 2. 并發(fā)標(biāo)記(CMS concurrent mark) 3. 重新標(biāo)記(CMS remark) 4. 并發(fā)清除(CMS concurrent sweep)
有以下幾個特點:
- 1: 其中,初試標(biāo)記皱坛、重新標(biāo)記這兩個步驟仍然需要 “Stop The World”编曼。
- 2: 初始標(biāo)記只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,速度很快剩辟。
- 3: 并發(fā)標(biāo)記階段就是進(jìn)行 GC Roots Tracing 的過程掐场。
- 4:重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,這個階段的停頓時間一般會比初試標(biāo)記階段稍長一些贩猎,但遠(yuǎn)比并發(fā)標(biāo)記的時間短熊户。
G1收集器
G1算法將堆劃分為若干個區(qū)域(Region),它仍然屬于分代收集器吭服。不過敏弃,這些區(qū)域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應(yīng)用線程的方式噪馏,將存活對象拷貝到老年代或者Survivor空間麦到。
老年代也分成很多區(qū)域绿饵,G1收集器通過將對象從一個區(qū)域復(fù)制到另外一個區(qū)域,完成了清理工作瓶颠。這就意味著拟赊,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮)粹淋,這樣也就不會有cms內(nèi)存碎片問題的存在了吸祟。
幾種收集器比較
收集器 | 特征 | 使用場景 |
---|---|---|
Serial 收集器 | 復(fù)制算法;單線程桃移;新生代屋匕;簡單而高效;需要進(jìn)行 stop the world借杰。 | 它是虛擬機運行在 Client 模式下的默認(rèn)新生代收集器 |
ParNew 收集器 | ParNew 收集器 | 它是許多運行在 Server 模式下的虛擬機中首選的新生代收集器过吻,其中有一個與性能無關(guān)但很重要的原因是,除了 Serial 收集器外蔗衡,目前只有它能與 CMS 收集器配合工作纤虽。 |
Parallel Scavenge 收集器 | 復(fù)制算法;并行多線程绞惦;新生代逼纸;吞吐量優(yōu)先原則;有自適應(yīng)調(diào)節(jié)策略 | 適合后臺運算而不需要太多交互的任務(wù) |
Serial Old 收集器 | 標(biāo)記-整理算法济蝉;老年代杰刽;單線程; | 這個收集器的主要意義在于給 Client 模式下的虛擬機使用 |
Parallel Old 收集器 | 標(biāo)記-整理王滤;老年代贺嫂;多線程;與 parallel scavenge 收集器結(jié)合實現(xiàn)吞吐量優(yōu)先 | 與 Parallel Scavenge 結(jié)合使用淑仆,適用那些注重吞吐量以及對 CPU 資源敏感的場合 |
CMS 收集器 | 標(biāo)記-清除涝婉;老年代;并發(fā)收集蔗怠、低停頓墩弯;有三個缺點 | 標(biāo)記-清除;老年代寞射;并發(fā)收集渔工、低停頓;有三個缺點 |
G1 收集器 | 分代收集桥温;空間整合引矩;可預(yù)測的停頓 | 面向服務(wù)器應(yīng)用垃圾收集器 |
分配策略
Java大部分對象指存活一小段時間,而小部分java對象存活時間長,
Java虛擬機堆劃分旺韭,將堆劃分為新生代和老年代氛谜。其中,新生代又被分為Eden區(qū)区端,以及兩個大小相等的Survivor區(qū)
默認(rèn)情況下值漫,Java 虛擬機采取的是一種動態(tài)分配的策略(對應(yīng) Java 虛擬機參數(shù) -XX:+UsePSAdaptiveSurvivorSizePolicy),根據(jù)生成對象
的速率织盼,以及 Survivor 區(qū)的使用情況動態(tài)調(diào)整 Eden區(qū)和 Survivor 區(qū)的比例杨何。
當(dāng)然,你也可以通過參數(shù) -XX:SurvivorRatio來固定這個比例沥邻。但是需要注意的是危虱,其中一個 Survivor
區(qū)會一直為空,因此比例越低浪費的堆空間將越高唐全。
對象優(yōu)先在Eden分配
大多數(shù)情況下埃跷,對象在新生代 Eden 區(qū)中分配。當(dāng) Eden 區(qū)沒有足夠空間進(jìn)行分配時芦瘾,虛擬機將發(fā)起一次 Minor GC捌蚊。
- 1: 新生代 GC( Minor GC): 指發(fā)生在新生代的垃圾收集動作集畅,因為 Java 對象大多都具備朝生夕滅的特性近弟,所以 Minor GC 非常頻繁,一般回收速度也比較快挺智。
- 2: 老年代 GC( Major GC/ Full GC): 指發(fā)生在老年代的 GC祷愉, 出現(xiàn)了 Major GC, 經(jīng)常會伴隨至少一次的 Minor GC(但非絕對的赦颇,在 Parallel Scavenge 收集器的收集策略里就有直接進(jìn)行 Major GC 的策略選擇過程)二鳄。 Major GC 的速度一般會比 Minor GC 慢 10 倍以上。
大對象直接進(jìn)入老年代
所謂的大對象是指:需要大量連續(xù)內(nèi)存空間的 Java 對象媒怯,最典型的大對象就是那種很長的字符串以及數(shù)組订讼。
大對象對虛擬機的內(nèi)存分配來說是一個壞消息,經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來“安置”它們扇苞。
-XX:PretenureSizeThreshold 參數(shù)欺殿,令大于這個設(shè)置值的對象直接在老年代分配(避免了在 Eden 以及兩個 Survivor 區(qū)之間發(fā)送大量的內(nèi)存復(fù)制)。
PretenureSizeThreshold 參數(shù)只對 Serial 和 ParNew 兩款收集器有效鳖敷, Parallel Scavenge 收集器不認(rèn)識這個參數(shù)脖苏。
長期存活的對象將進(jìn)入老年代
如果對象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然存活,并且能被 Survivor 容納的話定踱,將被移動到 Survivor 空間中棍潘,并且對象年齡設(shè)為 1。 對象在 Survivor 區(qū)中每熬過一次 Minor GC, 年齡就增加 1 歲亦歉,當(dāng)它的年齡增加到一定程度(默認(rèn)為 15 歲)恤浪,就將會被晉升到老年代中。
對象晉升老年代的年齡閾值肴楷,可以通過參數(shù) -XX: MaxTenuringThreshold 設(shè)置资锰。
空間分配擔(dān)保
有沒有想過,如果即時當(dāng)進(jìn)行小的對象分配的時候阶祭,年輕代沒有連續(xù)空間存放绷杜,但老年代有還有足夠空間,那怎么辦呢濒募?
這個時候就出現(xiàn)空間分配擔(dān)保鞭盟。
在發(fā)生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間瑰剃,
如果這個條件成立齿诉,那么 Minor GC 可以確保是安全的。當(dāng)大量對象在 Minor GC 后仍繞存活晌姚,
就需要老年代進(jìn)行空間分配擔(dān)保粤剧,把 Survivor 無法容納的對象直接進(jìn)入老年代。
如果老年代的判斷到剩余空間不足(根據(jù)以往每一次回收晉升到老年代對象容量的平均值作為經(jīng)驗值)挥唠,則進(jìn)行一次 Full GC抵恋。