JVM內(nèi)存管理與垃圾收集

1 運(yùn)行在Linux上的JVM

本文想講Java的內(nèi)存管理糯笙,首先花一點(diǎn)篇幅在OS的內(nèi)存上给涕。這里以大部分服務(wù)器運(yùn)行的基礎(chǔ)Linux為例够庙。

Linux內(nèi)存

學(xué)習(xí)過OS的都知道耘眨,一個(gè)CPU指令想要訪問或者寫內(nèi)存的某一個(gè)區(qū)域境肾,就會(huì)有一個(gè)尋址的操作奥喻。OS的內(nèi)存模型里环鲤,內(nèi)存的每一個(gè)區(qū)域都有一個(gè)地址冷离。而32位CPU的地址的位數(shù)限制在32位西剥,意即CPU只能在最大4GB(232)的空間內(nèi)完成尋址。而64位CPU可以完成更大范圍內(nèi)的尋址操作(264结耀,自己算算呢)图甜。當(dāng)前黑毅,我相信大部分的CPU與OS都已經(jīng)進(jìn)化到了64位矿瘦。 雖然CPU可以在這么寬廣的范圍內(nèi)尋址,但是實(shí)際機(jī)器的內(nèi)存可能并沒有那么大潮秘。所以枕荞,大部分現(xiàn)代的OS支持虛擬內(nèi)存躏精。比如我的機(jī)器有4GB RAM內(nèi)存矗烛,我可以假裝有8GB內(nèi)存瞭吃,其余4GB是虛擬內(nèi)存虱而,存儲(chǔ)在磁盤的交換區(qū)上(對(duì)于linux存儲(chǔ)在特殊的分區(qū)swap上开泽,相信自己裝過ubuntu的都知道)穆律。當(dāng)當(dāng)前任務(wù)使用的內(nèi)存超出物理內(nèi)存時(shí)峦耘,OS會(huì)根據(jù)一些算法把一部分最近不常使用的區(qū)域換出內(nèi)存到交換區(qū)辅髓,騰出來的空間給當(dāng)前急用洛口。對(duì)于Linux和大部分現(xiàn)代OS第焰,內(nèi)存交換的單位都是頁挺举。

還需要提一下OS的進(jìn)程模型湘纵。一個(gè)進(jìn)程擁有自己神圣不可侵犯的進(jìn)程空間。而一個(gè)進(jìn)程的空間被分為很多的頁脱篙,有可能一部分頁被OS調(diào)度到交換區(qū)绊困。顯而易見秤朗,這是這個(gè)進(jìn)程不想看到的事情取视。
下圖可以看到兩個(gè)JVM進(jìn)程作谭,都擁有自己的尋址空間折欠。在JVM服務(wù)中锐秦,可以通過-Xmx,-Xms來限制JVM進(jìn)程的最大內(nèi)存在物理內(nèi)存內(nèi)盗忱,并預(yù)留足夠空間給OS還有其他服務(wù)進(jìn)程扇谣。

image

如果內(nèi)存耗盡

地址空間耗盡一般發(fā)生在32位的機(jī)器上罐寨。內(nèi)存泄漏或過度使用本機(jī)內(nèi)存會(huì)迫使OS使用swap衩茸。訪問經(jīng)過交換的內(nèi)存地址比讀取駐留在物理內(nèi)存中的地址慢得多楞慈,因?yàn)楸仨殢拇疟P拉取數(shù)據(jù)囊蓝。可能會(huì)分配大量內(nèi)存來用完所有物理內(nèi)存和所有交換內(nèi)存(頁面空間)聚霜,在 Linux 上狡恬,這將觸發(fā)內(nèi)核內(nèi)存不足(OOM)結(jié)束程序,強(qiáng)制結(jié)束最消耗內(nèi)存的進(jìn)程蝎宇。
從JVM外簡單梳理了一下內(nèi)存管理后弟劲,我們可以來看看JVM內(nèi)部的內(nèi)存管理。

2 JVM內(nèi)存布局

JVM內(nèi)部的內(nèi)存布局可以看下圖姥芥。

image

大致可以分為3部分:

  • 方法區(qū)(Method Area) 所有線程共享的區(qū)域。
  • 堆(Heap) 所有線程共享的區(qū)域凉唐。
  • 線程的內(nèi)存空間區(qū)域庸追,包括:程序計(jì)數(shù)器(PC)虛擬機(jī)棧(VM Stack)台囱,本地方法棧(Native Method Stack)淡溯。

對(duì)于HotSpotVM,虛擬機(jī)棧和本地方法棧放在了一起簿训。

2.1 方法區(qū)

方法區(qū)域Java堆一樣咱娶,是各個(gè)線程共享的內(nèi)存區(qū)域,用于存儲(chǔ)已被虛擬機(jī)加載的類信息煎楣、常量豺总、靜態(tài)變量、JIT編譯后的代碼等數(shù)據(jù)择懂。

許多人稱這部分區(qū)域?yàn)橛谰么≒ermanent Generation)。但是二者并不等價(jià)另玖。HotSpot團(tuán)隊(duì)把GC分代收集擴(kuò)展到方法區(qū)困曙,使用永久代來實(shí)現(xiàn)方法區(qū)的回收和管理,避免專門為方法區(qū)編寫內(nèi)存管理代碼谦去。在其它的虛擬機(jī)實(shí)現(xiàn)并不存在永久代的概念慷丽。而HotSpot這樣的設(shè)計(jì)并不是一個(gè)很好的設(shè)計(jì),更容易遇到內(nèi)存溢出的問題鳄哭∫可以看下圖,只要方法區(qū)到達(dá)了XX:MaxPermSize上限就溢出了妆丘,但是锄俄,其它虛擬機(jī)局劲,如J9、JRockit只要沒有觸碰出可用內(nèi)存上限奶赠,就不會(huì)出現(xiàn)問題鱼填。在JDK 7中,已經(jīng)把字符串常量從永久代中移出毅戈,JDK 8中正式移除了永久代這個(gè)概念(JDK features),原先永生代中大部分內(nèi)容比如類的元信息會(huì)被放入本地內(nèi)存(Metaspace)苇经。

JVM規(guī)范對(duì)方法區(qū)的限制很寬松赘理,甚至可以選擇不實(shí)現(xiàn)垃圾收集。而垃圾收集在這個(gè)區(qū)域很少見扇单。主要內(nèi)存回收的目標(biāo)是針對(duì)常量池和對(duì)類型的卸載感憾。對(duì)類型的卸載非常苛刻令花。

運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分阻桅。Class文件中,除了有類的版本兼都、字段嫂沉、方法、接口等描述信息外扮碧,還有一項(xiàng)信息室常量池(Constant Pool Table)趟章,用于存放編譯期生成的各種字面量和符號(hào)引用。這部分內(nèi)容在類被加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放慎王。
而運(yùn)行時(shí)常量池相對(duì)于Class文件中的常量池的一個(gè)重要特征是具有動(dòng)態(tài)性蚓土。運(yùn)行期間也可以把新的常量放入池中。比如:String的intern方法赖淤。

String.intern
public String intern()
Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java Language Specification.
Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.

String.intern在java7之前蜀漆,過度使用可能會(huì)導(dǎo)致方法區(qū)(永久代)觸發(fā)OOM咱旱。Java7及之后將運(yùn)行時(shí)常量池被放到堆中管理,避免了這種情況。

2.2 方法棧

方法棧有兩種基公,JVM棧與本地方法棧(Native Method Stacks)酸休。它是線程私有的互站,生命周期與線程相同翠胰。每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)存儲(chǔ)局部變量表、操作數(shù)棧戏罢、動(dòng)態(tài)鏈接、方法出口等讲岁。局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型、對(duì)象引用和returnAddress類型(指向一條字節(jié)碼指令的地址)。

局部變量表所需的內(nèi)存空可以在編譯期間完全確定浮禾,runtime它的大小不會(huì)被改變。當(dāng)線程請(qǐng)求的棧深度大于設(shè)定閾值均践,JVM會(huì)拋出StackOverflowError焦影;如果無法申請(qǐng)到足夠內(nèi)存完成棧的動(dòng)態(tài)擴(kuò)展车遂,將會(huì)拋出OutOfMemoryError異常。

本地方法棧為VM使用的本地方法服務(wù)斯辰。虛擬機(jī)規(guī)范并未對(duì)其使用語言、方式衣陶、數(shù)據(jù)結(jié)構(gòu)有強(qiáng)制規(guī)定剪况,而許多VM實(shí)現(xiàn)教沾,包括HotSpot虛擬機(jī),把VM棧和本地方法棧合二為一译断,一樣也可能拋出StackOverflowError或者OOM授翻。

2.3 程序計(jì)數(shù)器

程序計(jì)數(shù)器是每個(gè)線程私有的用于記錄當(dāng)前線程所執(zhí)行字節(jié)碼的行號(hào)指示器。在VM的概念模型中孙咪,字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器來選取下一條需要執(zhí)行的字節(jié)碼指令堪唐,分支、循環(huán)该贾、跳轉(zhuǎn)羔杨、異常處理、線程恢復(fù)等都依賴這個(gè)計(jì)數(shù)器完成杨蛋。當(dāng)線程執(zhí)行java方法時(shí)兜材,它存儲(chǔ)正在執(zhí)行的字節(jié)碼的地址;如果是本地方法逞力,它的值為undefined曙寡。此內(nèi)存區(qū)域是JVM規(guī)范中沒有規(guī)定任何OOM情況的區(qū)域。

2.4 堆

堆應(yīng)該是內(nèi)存管理中最大的一塊寇荧,被所有線程共享举庶。所有的對(duì)象實(shí)例都在堆上分配。但是揩抡,隨著JIT的發(fā)展與逃逸分析技術(shù)的成熟户侥,棧上分配、標(biāo)量替換優(yōu)化技術(shù)會(huì)導(dǎo)致一些微妙變化發(fā)生峦嗤,所有對(duì)象在堆上分配便不那么絕對(duì)了蕊唐。

現(xiàn)代的垃圾收集器基本采用分代收集算法,可以參考下圖:

image

分為新生代和老年代烁设,還可以細(xì)分為:Eden替梨,F(xiàn)rom Survivor,To Survivor装黑,等副瀑。線程共享的java堆中,可能會(huì)劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation buffer恋谭,TLAB)糠睡。進(jìn)一步劃分的目的是為了更好地回收內(nèi)存,或更快地分配內(nèi)存疚颊。
而Java堆可以處于物理上不連續(xù)的空間中铜幽。

2.5 其它內(nèi)存占用

還有一些其它的內(nèi)存占用值得注意滞谢。直接內(nèi)存(Direct Memory)不是VM運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分串稀,也不是VM規(guī)范定義的內(nèi)存區(qū)域除抛。但是它也會(huì)被頻繁使用,可能導(dǎo)致OOM母截。
JDK 1.4中加入的NIO(Nonblocking IO)引入了基于通道Channel與緩沖區(qū)(Buffer)的IO方式到忽。它使用native函數(shù)庫直接分配堆外內(nèi)存,通過存在Java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用來操作清寇。在許多場(chǎng)景中喘漏,這樣的設(shè)計(jì)因避免在Java堆與native對(duì)中來回復(fù)制而提高了性能。但是它的分配不會(huì)受到Java堆大小的限制华烟,但還是會(huì)收到本機(jī)內(nèi)存的限制翩迈。如果在配置JVM參數(shù)時(shí)候忽略了直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制盔夜,會(huì)導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)OOM负饲。

3 自動(dòng)內(nèi)存管理

這里我們以官方的HotSpot虛擬機(jī)為例,看JVM如何自動(dòng)管理內(nèi)存喂链。

3.1 對(duì)象的創(chuàng)建

對(duì)象的創(chuàng)建可以分為如下的過程:

  1. 檢查并類加載如果需要
  2. 分配內(nèi)存空間
  3. 初始化零值(可能會(huì)在TLAB分配新空間時(shí)做)
  4. 對(duì)象頭設(shè)置元信息

JVM在遇到一條new指令時(shí)返十,首先檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,這個(gè)類是否被加載椭微、解析和初始化過洞坑。如果沒有,這執(zhí)行類加載過程蝇率。
加載類完畢后迟杂,接下來VM將會(huì)為新對(duì)象分配內(nèi)存埃儿。對(duì)象所需要的內(nèi)存大小在類加載完成后便可完全確定螺戳。
分配對(duì)象有兩種情況:

  1. 如果Java堆內(nèi)存絕對(duì)規(guī)整 使用一個(gè)指針指向已經(jīng)分配的內(nèi)存區(qū)域和未分配內(nèi)存區(qū)域的地址,新的對(duì)象創(chuàng)建只需要將這個(gè)指針向空閑區(qū)域方向移動(dòng)給定空間袍患。這種分配方式被稱為『指針碰撞』间狂。
  2. 如果Java內(nèi)存并不規(guī)整 『指針碰撞』無法適用攻泼。VM需要維護(hù)一個(gè)列表,記錄哪些內(nèi)存塊是可用的鉴象,在列表中找到足夠大的空間分配給對(duì)象忙菠。這種方法叫『空閑列表』。

Java堆是否規(guī)整由采用的垃圾收集器是否帶有壓縮整理功能決定纺弊。CMS默認(rèn)使用空閑列表牛欢,而Serial、parNew等帶compat過程的收集器就采用指針碰撞淆游。JVM啟動(dòng)時(shí)的參數(shù)XX+UseCMSCompactAtFullCollection可以在CMS垃圾收集器中開啟Full GC時(shí)整理傍睹、壓縮堆內(nèi)存隔盛。

對(duì)象創(chuàng)建是VM中非常頻繁的行為,并發(fā)情況下如何保證一致性拾稳,通常有兩種解決方案:

  1. 加鎖同步 通過VM使用CAS(java.util.concurrent包中借助CAS實(shí)現(xiàn)了區(qū)別于synchronize同步鎖的一種樂觀鎖)配上失敗重試來保證內(nèi)存分配的原子性吮炕。
  2. 分配TLAB劃分不同線程的內(nèi)存空間 將不同線程的內(nèi)存分配隔離在不同的內(nèi)存區(qū)域。每個(gè)線程擁有一個(gè)內(nèi)存分配緩沖(TLAB访得,Thread Local Allocation Buffer)龙亲。 只有在TLAB用盡分配新的空間給TLAB時(shí)候才需要同步鎖定『芬郑可以通過-XX:/-UseTLAB參數(shù)設(shè)定是否使用TLAB鳄炉。

內(nèi)存分配完畢后,VM會(huì)將所有空間都初始化為零值(不包括對(duì)象頭)搜骡。然后VM會(huì)對(duì)對(duì)象頭做初始化拂盯。對(duì)于VM來說,一個(gè)新的對(duì)象已經(jīng)誕生了记靡;但從Java的角度谈竿,對(duì)象創(chuàng)建才剛剛開始——開始執(zhí)行<init>方法,按照程序員的意愿初始化數(shù)據(jù)簸呈。

而對(duì)象的回收才是進(jìn)入到JVM的垃圾回收器的工作原理榕订。

3.2 回收算法概述

基本的垃圾收集算法由4種:

  • 標(biāo)記-清除法(Mark-Sweep) 首先標(biāo)記全部需要回收的對(duì)象,標(biāo)記完成后統(tǒng)一回收蜕便。他的不足在于:標(biāo)記劫恒、清除的過程效率并不高;同時(shí)會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片轿腺。

    image

  • 復(fù)制算法(Copy) 它將內(nèi)存劃分為兩個(gè)部分两嘴,每次只使用一塊。當(dāng)這塊要用完了族壳,就把還存活的對(duì)象復(fù)制到另一塊上面憔辫;原來的一塊全部清理掉。這樣仿荆,可以使用『指針碰撞』的方法順序地分配內(nèi)存贰您。缺點(diǎn)是一半的內(nèi)存無法使用,并且在對(duì)象存活率較高的時(shí)候效率較低拢操〗跻啵可以參考下圖。

    image

  • 標(biāo)記整理法(mark-Compact) 標(biāo)記整理類似于標(biāo)記清除令境,不同的是杠园,標(biāo)記之后不是清除(Sweep),而是讓存活對(duì)象都向一端移動(dòng)舔庶,然后直接清除掉端邊界之外的內(nèi)存抛蚁。

    image

    本節(jié)圖片均來自于《深入理解jVM虛擬機(jī)》3.3

3.3 堆分代收集

如下圖所示陈醒,JVM主要會(huì)被分為兩個(gè)部分:年輕代老年代
[圖片上傳失敗...(image-e5966-1510413201979)]
把對(duì)象按照不同的年齡分離開瞧甩,使用不同的策略清理钉跷,能夠提高垃圾收集器的效率。

JVM的開發(fā)者發(fā)現(xiàn)亲配,一個(gè)程序中的大部分對(duì)象都會(huì)在年輕的時(shí)候死亡尘应。IBM專門的研究表明,年輕代98%的對(duì)象都是朝生夕死的吼虎。不需要按照1:1比例劃分內(nèi)存空間。
年輕代又分為Eden和兩個(gè)大小相等的Survivor區(qū)域:from和to苍鲜。每次使用Eden和一塊Survivor思灰。新的對(duì)象在Eden里分配。當(dāng)Eden空間不足時(shí)混滔,會(huì)發(fā)生一次Minor GC洒疚。
Minor GC將在使用的Survivor和Eden存活的對(duì)象一次性復(fù)制到另一個(gè)Survivor中,然后清理Eden和之前的Survivor坯屿。HotSpot虛擬機(jī)默認(rèn)Eden:Survivor大小比例是8:1油湖。這樣,年輕代自浪費(fèi)了10%的空間领跛。當(dāng)然乏德,當(dāng)Survivor空間不夠用時(shí),需要依賴?yán)夏甏═enured)進(jìn)行分配擔(dān)保(Handle Promotion)吠昭。
一個(gè)對(duì)象的年齡即它經(jīng)歷的Minor GC次數(shù)喊括。在年齡到達(dá)一定次數(shù)時(shí),對(duì)象會(huì)被移動(dòng)到老年代(Promotion)矢棚。發(fā)生分配擔(dān)保時(shí)也會(huì)有年輕代對(duì)象被移到老年代郑什。

而由于老年代的對(duì)象存活率較高,沒有額外空間進(jìn)行分配擔(dān)保蒲肋,必須使用『標(biāo)記-清除』或者『標(biāo)記-整理』算法來回收蘑拯。

3.4 如何標(biāo)記

HotSpot實(shí)現(xiàn)上述的算法時(shí),必須要保證高效的執(zhí)行效率兜粘。

如何標(biāo)記存活實(shí)例呢申窘?目前主要有兩種方法:

  1. 引用計(jì)數(shù)法 VM維護(hù)每個(gè)對(duì)象被引用的次數(shù),引用次數(shù)為0的對(duì)象被標(biāo)記清除妹沙。這個(gè)算法足夠簡單偶洋,但是無法回收循環(huán)依賴的對(duì)象。Java并沒有采用這樣的算法距糖。
  2. 標(biāo)記存活對(duì)象 從一系列根節(jié)點(diǎn)出發(fā)玄窝,根據(jù)引用關(guān)系遍歷對(duì)象牵寺,遍歷的對(duì)象標(biāo)記為存活對(duì)象。剩余對(duì)象為需要回收的對(duì)象恩脂。正是因?yàn)榇蟛糠謱?duì)象都是朝生暮死帽氓,所以找活著的比找死去的容易一些。

那么俩块,對(duì)于JVM來說黎休,哪些是根節(jié)點(diǎn)對(duì)象呢?

  • 方法棧中的對(duì)象引用
  • static靜態(tài)變量 只有類被VM卸載的時(shí)候(類和方法保存在方法區(qū)玉凯,即Metaspace势腮,原Perm),static變量才會(huì)被回收漫仆。
  • main thread對(duì)象
  • JNI引用

Java會(huì)從這些根節(jié)點(diǎn)對(duì)象開始掃描存活對(duì)象捎拯。實(shí)際上,HotSpot為了更加高效地實(shí)現(xiàn)標(biāo)記算法盲厌,在編譯期間與運(yùn)行期間都使用了很多手段降低標(biāo)記運(yùn)行時(shí)間署照。可達(dá)性分析對(duì)執(zhí)行時(shí)間非常敏感吗浩,尤其是GC停頓的時(shí)候建芙。可達(dá)性分析需要在一個(gè)能夠確保一致性的堆內(nèi)存快照中進(jìn)行懂扼,在分析過程中不會(huì)出現(xiàn)對(duì)象引用關(guān)系仍然在不斷變化的情況禁荸。此時(shí)便會(huì)停頓所有執(zhí)行線程(Stop the World)。在幾乎不停頓的CMS收集器中仍然會(huì)在枚舉根節(jié)點(diǎn)的時(shí)候停頓微王。
同時(shí)屡限,JVM會(huì)保證任何停頓都發(fā)生在SafePoint或者SafeRegion。

具體JVM針對(duì)枚舉根節(jié)點(diǎn)過程的優(yōu)化可以參考《深入理解JVM虛擬機(jī)》3.4節(jié)炕倘。

3.5 垃圾回收器

下圖包含了HotSpot虛擬機(jī)的全部虛擬機(jī)钧大,還有不同回收器之間是否可以配合使用(用線連接)。而不同的收集器在不同的區(qū)域使用罩旋。
[圖片上傳失敗...(image-41cc80-1510413201979)]
CMS和G1是兩個(gè)比較復(fù)雜的收集器啊央,G1更是在JDK 7 Update14后在移除了實(shí)驗(yàn)的標(biāo)簽。沒有完美的收集器涨醋,而是針對(duì)具體應(yīng)用最適合的收集器瓜饥。

新生代回收器

新生代收集器包含3種:Serial,ParNew浴骂,Parrallel Scavenge乓土。

Serial

[圖片上傳失敗...(image-7edf57-1510413201979)]
一圖省千言。單GC線程,Stop the World趣苏,但是Serial收集器仍然是Java client模式下默認(rèn)的年輕代垃圾收集器狡相,簡單而高效。

ParNew

ParNew其實(shí)就是Serial的多線程版本食磕,除了多GC線程外尽棕,還可以通過JVM啟動(dòng)參數(shù)調(diào)整,如:-XX:SurvivorRatio, -XX:PretenureSizeThreshold, -XX:HandlePromotionFailure等彬伦。
ParNew是用戶使用-XX:+UseConcMarkSweepGC來使用CMS作為老年代的收集器時(shí)滔悉,ParNew是默認(rèn)年輕代的收集器。

Parrallel Scavenge

它也是復(fù)制算法的收集器单绑,同時(shí)是并行的回官。與ParNew相比的特別之處在于,它的目標(biāo)是要達(dá)到一個(gè)可控制的吞吐量询张,而其他收集器如CMS關(guān)注于盡可能縮短垃圾收集時(shí)用戶線程停頓的時(shí)間孙乖。
所謂的吞吐量,是CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值份氧。

Parrallel Scavenge提供了比較豐富的可控制的參數(shù),并且自己有一套自適應(yīng)的調(diào)節(jié)策略弯屈。

老年代回收器

老年代包含:Serial Old收集器蜗帜,Parrallel Old收集器,和CMS资厉。CMS比較復(fù)雜厅缺,應(yīng)用比較廣泛,單獨(dú)來說宴偿。

Serial Old是單線程采用標(biāo)記-整理算法的收集器湘捎,會(huì)Stop the World后單GC線程標(biāo)記-整理。它也是Client模式下的默認(rèn)老年代收集器窄刘。在JDK 5以及之前與Parrallel Scavenge搭配使用窥妇。另一個(gè)作用是作為CMS的后備方案,在發(fā)生Concurrent Mode Failure的時(shí)候使用娩践。

Parrallel Old是Parrallel Scavenge的老年代版本活翩,使用多線程和標(biāo)記-整理算法。從JDK6開始提供翻伺。Parrallel Old配合Parrallel Scavenge成為了『吞吐量優(yōu)先』收集器的應(yīng)用組合材泄,用于注重吞吐量和CPU資源敏感的場(chǎng)合

CMS

CMS全稱為Concurrent Mark-Sweep吨岭,是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器拉宗。被廣泛用于互聯(lián)網(wǎng)的后端服務(wù)等注重服務(wù)響應(yīng)速度的場(chǎng)合。
CMS是基于標(biāo)記-清除算法實(shí)現(xiàn)的,過程比較復(fù)雜旦事,整個(gè)過程可以分為4步魁巩,其中初始標(biāo)記與重新標(biāo)記步驟需要Stop the World,二者的耗時(shí)得到了比較好的控制族檬,總體上來說CMS是HotSpot虛擬機(jī)第一款真正意義上的并發(fā)收集器歪赢,第一次實(shí)現(xiàn)了GC線程與用戶線程同時(shí)工作,是具有劃時(shí)代意義的单料。

下面按照4個(gè)步驟分解一下收集器的收集過程埋凯。

CMS初始標(biāo)記 initial mark

初始標(biāo)記比較簡單,標(biāo)記一下GC根節(jié)點(diǎn)能夠關(guān)聯(lián)到的對(duì)象扫尖,速度很快白对,但是需要Stop the World。

CMS并發(fā)標(biāo)記 concurrent mark

并發(fā)標(biāo)記换怖,JVM會(huì)開啟多線程與用戶線程一起工作甩恼。進(jìn)行GC根節(jié)點(diǎn)的tracing過程。在trace過程中沉颂,堆中對(duì)象的引用關(guān)系可能在不斷變化条摸。

CMS重新標(biāo)記 remark

remark需要再次Stop the World,為了修正并發(fā)標(biāo)記過程中因用戶程序繼續(xù)運(yùn)作導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄铸屉。耗時(shí)會(huì)比初始標(biāo)記長钉蒲,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。

CMS并發(fā)清除 concurrent sweep

并發(fā)清除就是與用戶線程一起同時(shí)對(duì)標(biāo)記完后未被標(biāo)記的對(duì)象做清理彻坛。此時(shí)顷啼,因?yàn)椴粫?huì)做整理,所以會(huì)產(chǎn)生不少的碎片昌屉。

整個(gè)過程可以看下圖钙蒙。
[圖片上傳失敗...(image-53e113-1510413201979)]

CMS的缺陷與常見問題

  1. CMS對(duì)CPU資源非常敏感。CMS雖然會(huì)與用戶線程并發(fā)執(zhí)行间驮,但是仍然會(huì)占用一部分CPU資源躬厌,造成吞吐降低。CMS默認(rèn)啟動(dòng)的回收線程數(shù)是(CPU數(shù)量+3)/4蜻牢。尤其是CPU核數(shù)較小的時(shí)候?qū)τ脩舫绦蛴绊懕容^明顯烤咧。

  2. CMS無法處理浮動(dòng)垃圾,可能出現(xiàn)Concurrent Mode Failure進(jìn)而導(dǎo)致另一次Full GC發(fā)生抢呆。在CMS并發(fā)清除時(shí)煮嫌,用戶程序還在運(yùn)行,仍然會(huì)產(chǎn)生垃圾抱虐。這部分垃圾并不會(huì)在這次GC過程中被回收昌阿。它們就叫浮動(dòng)垃圾。在過程期間,CMS需要預(yù)留足夠空間給用戶的程序使用(這個(gè)值可以通過JVM參數(shù)-XX:CMSInitiatingoccupancyFraction設(shè)定懦冰,JDK 6中為92%)灶轰。如果預(yù)留空間無法滿足用戶程序需要,就會(huì)發(fā)生Concurrent Mode Failure刷钢。此時(shí)笋颤,JVM啟動(dòng)預(yù)案,即使用Serial Old收集器重新進(jìn)行老年代的垃圾收集内地,導(dǎo)致停頓時(shí)間非常長伴澄。

  3. 當(dāng)進(jìn)行Handle Promotion的時(shí)候,survivor空間放不下年輕代的存活對(duì)象阱缓,對(duì)象只能放入老年代非凌,但是老年代也放不下的時(shí)候,就會(huì)出現(xiàn) 擔(dān)保失斁U搿(Promotion Failure)敞嗡。
    擔(dān)保失敗的出現(xiàn)很可能是因?yàn)槔夏甏鷥?nèi)存碎片太多,而新生代來的對(duì)象比較大造成航背。此時(shí)會(huì)提前觸發(fā)stop the world的CMS的Full GC過程喉悴,但是CMS默認(rèn)使用標(biāo)記-清除算法,并不會(huì)整理玖媚≈嗑澹可以通過Java啟動(dòng)參數(shù)-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5來開啟CMS算法的每5次Full GC進(jìn)行一次標(biāo)記整理,從而控制老年代的碎片最盅。
    下圖是promotion failure的一條gc日志:

2016-11-18T07:26:09.665+0800: 39287.719: [GC2016-11-18T07:26:09.666+0800: 39287.719: [ParNew (promotion failed)
: 557937K->566208K(566208K), 0.1302900 secs]2016-11-18T07:26:09.796+0800: 39287.850: [CMS: 2310524K->290069K(2516608K), 1.4146810 secs] 2855851K->290069K(3082816K), [CMS Perm : 117537K->116340K(195872K)], 1.5457950 secs] [Times: user=1.67 sys=0.00, real=1.54 secs]

Garbage First (G1)

G1是面向服務(wù)端應(yīng)用,未來的目標(biāo)是全面替代JDK 5發(fā)布的CMS收集器起惕。
相比其他收集器涡贱,G1有如下特點(diǎn):

  1. 并行與并發(fā) 充分利用多核縮短Stop the World停頓時(shí)間。
  2. 分代收集 繼承了分代收集特點(diǎn)惹想,無需其他收集器配合就可以獨(dú)立管理整個(gè)GC堆问词。
  3. 空間整合 整體看采用標(biāo)記-整理算法實(shí)現(xiàn)。局部上看是通過復(fù)制實(shí)現(xiàn)嘀粱。不會(huì)產(chǎn)生空間碎片激挪。有利于程序長時(shí)間運(yùn)行,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o法找到連續(xù)內(nèi)存空間而提前觸發(fā)GC锋叨。
  4. 可預(yù)測(cè)的停頓 可以建立可預(yù)測(cè)的停頓時(shí)間模型垄分,讓使用者明確指定一個(gè)長度為M毫秒的時(shí)間片段內(nèi),消耗在GC上的時(shí)間不得超過N毫秒娃磺。做到這點(diǎn)是因?yàn)楸∈珿1可以有計(jì)劃避免在整個(gè)Java堆進(jìn)行全區(qū)域GC。

G1的堆的內(nèi)存布局不再是連續(xù)的年輕代與老年代了,而是如下圖這樣的被分為多個(gè)大小相等的獨(dú)立region豺瘤。G1會(huì)跟蹤各個(gè)region里垃圾堆積的價(jià)值大小吆倦,維護(hù)優(yōu)先隊(duì)列,根據(jù)允許的收集時(shí)間坐求,優(yōu)先回收最大的region蚕泽。

image

其實(shí),實(shí)現(xiàn)的復(fù)雜度很高桥嗤。Java堆雖然被分為了多個(gè)region须妻,卻不可能是互相孤立的,一個(gè)region的對(duì)象可能與整個(gè)java堆的任意region的對(duì)象有引用關(guān)系砸逊。在做可達(dá)性判定時(shí)還是無法避免掃描整個(gè)堆璧南。在各個(gè)收集器都有類似的問題,G1尤其突出师逸。
G1使用Recommended Set來避免全堆掃描司倚。具體細(xì)節(jié)不表,參考《深入理解JVM虛擬機(jī)》3.5.7篓像。

G1的處理流程也可以大致分為以下幾步:

  1. 初始標(biāo)記 initial marking
  2. 并發(fā)標(biāo)記 concurrent marking
  3. 最終標(biāo)記 final marking
  4. 篩選回收 live data counting and evacuation

[圖片上傳失敗...(image-3537c-1510413201979)]
具體的過程我還需要研究一下动知,暫且留個(gè)坑在這里。
//TODO

References

  1. 《深入理解JVM虛擬機(jī)》
  2. 胖胖的回答-Java 的垃圾回收機(jī)制的主要原理是什么员辩?什么情況下會(huì)影響到程序的運(yùn)行效率盒粮?
  3. Java內(nèi)存詳解
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市奠滑,隨后出現(xiàn)的幾起案子丹皱,更是在濱河造成了極大的恐慌,老刑警劉巖宋税,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摊崭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡杰赛,警方通過查閱死者的電腦和手機(jī)呢簸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乏屯,“玉大人根时,你說我怎么就攤上這事〕皆危” “怎么了蛤迎?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伞芹。 經(jīng)常有香客問我忘苛,道長蝉娜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任扎唾,我火速辦了婚禮召川,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胸遇。我一直安慰自己荧呐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布纸镊。 她就那樣靜靜地躺著倍阐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逗威。 梳的紋絲不亂的頭發(fā)上峰搪,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音凯旭,去河邊找鬼概耻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛罐呼,可吹牛的內(nèi)容都是我干的鞠柄。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼嫉柴,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼厌杜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起计螺,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤夯尽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后登馒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呐萌,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年谊娇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片罗晕。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡济欢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出小渊,到底是詐尸還是另有隱情法褥,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布酬屉,位于F島的核電站半等,受9級(jí)特大地震影響揍愁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杀饵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一莽囤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧切距,春花似錦朽缎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至葡幸,卻和暖如春最筒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蔚叨。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工床蜘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缅叠。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓悄泥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親肤粱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子弹囚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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