Java垃圾回收

對于垃圾回收霞幅,對于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抵恋。

參考:
博客圓 極客時間 星哥
個人博客
《深入理解Java虛擬機》周志明

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市宝磨,隨后出現(xiàn)的幾起案子弧关,更是在濱河造成了極大的恐慌,老刑警劉巖唤锉,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件世囊,死亡現(xiàn)場離奇詭異,居然都是意外死亡窿祥,警方通過查閱死者的電腦和手機株憾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事诀蓉。” “怎么了猫胁?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長跛锌。 經(jīng)常有香客問我弃秆,道長届惋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任菠赚,我火速辦了婚禮脑豹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衡查。我一直安慰自己瘩欺,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布拌牲。 她就那樣靜靜地躺著俱饿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪塌忽。 梳的紋絲不亂的頭發(fā)上拍埠,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天,我揣著相機與錄音土居,去河邊找鬼枣购。 笑死,一個胖子當(dāng)著我的面吹牛擦耀,可吹牛的內(nèi)容都是我干的棉圈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼眷蜓,長吁一口氣:“原來是場噩夢啊……” “哼分瘾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起账磺,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤芹敌,失蹤者是張志新(化名)和其女友劉穎痊远,沒想到半個月后垮抗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡碧聪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年冒版,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逞姿。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡辞嗡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滞造,到底是詐尸還是另有隱情续室,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布谒养,位于F島的核電站挺狰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丰泊,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一薯定、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞳购,春花似錦话侄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盏浇,卻和暖如春嘀韧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缠捌。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工锄贷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人曼月。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓谊却,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哑芹。 傳聞我的和親對象是個殘疾皇子炎辨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容