JVM 垃圾回收原理

前言

?? JVM 是 Java Virtual Machine(Java 虛擬機(jī))的縮寫,它也是一個(gè)虛構(gòu)出來的計(jì)算機(jī)黎炉,是通過在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來完成的娄柳。Java 語(yǔ)言最重要的特性之一的自動(dòng)垃圾回收機(jī)制注竿,也是基于 JVM 實(shí)現(xiàn)的压状,而且在我們?nèi)粘i_發(fā)中經(jīng)常會(huì)遇到內(nèi)存溢出、內(nèi)存泄漏甫窟、程序卡頓等問題密浑,要針對(duì)這些問題進(jìn)行排查和性能調(diào)優(yōu),就需要我們了解 JVM 的垃圾回收機(jī)制(Garbage Collection,GC)粗井,那么垃圾回收機(jī)制到底是如何實(shí)現(xiàn)的呢肴掷?接下來讓我們一探究竟。

一. 運(yùn)行時(shí)數(shù)據(jù)區(qū)域

?? 在了解 JVM 的垃圾回收機(jī)制之前背传,我們需要先了解 JVM 運(yùn)行時(shí)的數(shù)據(jù)區(qū)域是怎么劃分的呆瞻,才能知道在哪里產(chǎn)生和回收垃圾。

?? Java 虛擬機(jī)在執(zhí)行 Java 程序的過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域径玖。這些區(qū)域都有各自的用途痴脾,以及創(chuàng)建和銷毀的時(shí)間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在梳星,有些區(qū)域則依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀赞赖。根據(jù)《Java 虛擬機(jī)規(guī)范(Java SE 7 版)》的規(guī)定,Java 虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域冤灾,如圖所示前域。

1. 程序計(jì)數(shù)器

程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器韵吨。在虛擬機(jī)的概念模型里(僅是概念模型匿垄,各種虛擬機(jī)可能會(huì)通過一些更高效的方式去實(shí)現(xiàn)),字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令归粉,分支椿疗、循環(huán)、跳轉(zhuǎn)糠悼、異常處理届榄、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。

由于 Java 虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的倔喂, 在任何一個(gè)確定的時(shí)刻铝条,一個(gè)處理器(對(duì)于多核處理器來說是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此席噩,為了線程切換后能恢復(fù)到正確的執(zhí)行位置班缰,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響班挖,獨(dú)立存儲(chǔ)鲁捏,我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存芯砸。 如果線程正在執(zhí)行的是一個(gè) Java 方法萧芙,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址给梅;如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)双揪。此內(nèi)存區(qū)域是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域动羽。

2. Java 虛擬機(jī)棧

與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的渔期,它的生命周期與線程相同运吓。虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧疯趟、動(dòng)態(tài)鏈接拘哨、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程信峻,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程倦青。 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte盹舞、char产镐、short、int踢步、 float癣亚、long、double)获印、對(duì)象引用( reference 類型述雾,它不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔樇娣幔部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和 returnAddress 類型(指向了一條字節(jié)碼指令的地址)绰咽。 其中 64 位長(zhǎng)度的 long 和 double 類型的數(shù)據(jù)會(huì)占用 2 個(gè)局部變量空間(Slot),其余的數(shù)據(jù)類型只占用 1 個(gè)地粪。局部變量表所需的內(nèi)存空間在編譯期間完成分配取募,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的蟆技,在方法運(yùn)行期間不會(huì)改變局部變量表的大小玩敏。 在 Java 虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度质礼,將拋出 StackOverflowError 異常旺聚;如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展(當(dāng)前大部分的 Java 虛擬機(jī)都可動(dòng)態(tài)擴(kuò)展眶蕉,只不過 Java 虛擬機(jī)規(guī)范中也允許固定長(zhǎng)度的虛擬機(jī)棧)砰粹,如果擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出 OutOfMemoryError 異常造挽。

3. 本地方法棧

?? 本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的碱璃,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù)弄痹,而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。在虛擬機(jī)規(guī)范中對(duì)本地方法棧中方法使用的語(yǔ)言嵌器、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定肛真,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它。甚至有的虛擬機(jī)(譬如 Sun HotSpot 虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一爽航。與虛擬機(jī)棧一樣蚓让,本地方法棧區(qū)域也會(huì)拋出 StackOverflowError 和 OutOfMemoryError 異常。

4. Java堆

對(duì)于大多數(shù)應(yīng)用來說讥珍,Java 堆(Java Heap)是 Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊历极。 Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建衷佃。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例执解,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。這一點(diǎn)在 Java 虛擬機(jī)規(guī)范中的描述是:所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配纲酗,但是隨著 JIT 編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟衰腌,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化發(fā)生觅赊,所有的對(duì)象都分配在堆上也漸漸變得不是那么“絕對(duì)”了右蕊。 Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱做“ GC 堆”(Garbage Collected Heap)吮螺。從內(nèi)存回收的角度來看饶囚,由于現(xiàn)在收集器基本都采用分代收集算法,所以 Java 堆中還可以細(xì)分為:新生代和老年代鸠补;再細(xì)致一點(diǎn)的有 Eden 空間萝风、From Survivor 空間、To Survivor 空間等紫岩。從內(nèi)存分配的角度來看规惰,線程共享的 Java 堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。不過無論如何劃分泉蝌,都與存放內(nèi)容無關(guān)歇万,無論哪個(gè)區(qū)域,存儲(chǔ)的都仍然是對(duì)象實(shí)例勋陪,進(jìn)一步劃分的目的是為了更好地回收內(nèi)存贪磺,或者更快地分配內(nèi)存。 根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定诅愚,Java 堆可以處于物理上不連續(xù)的內(nèi)存空間中寒锚,只要邏輯上是連續(xù)的即可,就像我們的磁盤空間一樣。在實(shí)現(xiàn)時(shí)刹前,既可以實(shí)現(xiàn)成固定大小的泳赋,也可以是可擴(kuò)展的,不過當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過 -Xmx 和 -Xms 控制)腮郊。如果在堆中沒有內(nèi)存完成實(shí)例分配摹蘑,并且堆也無法再擴(kuò)展時(shí)筹燕,將會(huì)拋出 OutOfMemoryError 異常轧飞。

5. 方法區(qū)

?? 方法區(qū)(Method Area)與 Java 堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域撒踪,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息过咬、常量、靜態(tài)變量制妄、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)掸绞。雖然 Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分面殖,但是它卻有一個(gè)別名叫做 Non-Heap(非堆)捍歪,目的應(yīng)該是與 Java 堆區(qū)分開來。 Java 虛擬機(jī)規(guī)范對(duì)方法區(qū)的限制非常寬松妆棒,除了和 Java 堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外俺抽,還可以選擇不實(shí)現(xiàn)垃圾收集敞映。相對(duì)而言,垃圾收集行為在這個(gè)區(qū)域是比較少出現(xiàn)的磷斧,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣“永久”存在了振愿。這區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載,一般來說弛饭,這個(gè)區(qū)域的回收“成績(jī)”比較難以令人滿意冕末,尤其是類型的卸載,條件相當(dāng)苛刻侣颂,但是這部分區(qū)域的回收確實(shí)是必要的档桃。 根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)憔晒,將拋出 OutOfMemoryError 異常胳蛮。

6. 運(yùn)行時(shí)常量池

?? 運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本丛晌、字段仅炊、方法、接口等描述信息外澎蛛,還有一項(xiàng)信息是常量池(Constant Pool Table)抚垄,用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。 Java 虛擬機(jī)對(duì) Class 文件每一部分(自然也包括常量池)的格式都有嚴(yán)格規(guī)定呆馁,每一個(gè)字節(jié)用于存儲(chǔ)哪種數(shù)據(jù)都必須符合規(guī)范上的要求才會(huì)被虛擬機(jī)認(rèn)可桐经、裝載和執(zhí)行,但對(duì)于運(yùn)行時(shí)常量池浙滤,Java 虛擬機(jī)規(guī)范沒有做任何細(xì)節(jié)的要求阴挣,不同的提供商實(shí)現(xiàn)的虛擬機(jī)可以按照自己的需要來實(shí)現(xiàn)這個(gè)內(nèi)存區(qū)域。不過纺腊,一般來說畔咧,除了保存 Class 文件中描述的符號(hào)引用外, 還會(huì)把翻譯出來的直接引用也存儲(chǔ)在運(yùn)行時(shí)常量池中揖膜。 運(yùn)行時(shí)常量池相對(duì)于 Class 文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性誓沸,Java 語(yǔ)言并不要求常量一定只有編譯期才能產(chǎn)生,也就是并非預(yù)置入 Class 文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池壹粟,運(yùn)行期間也可能將新的常量放入池中拜隧,這種特性被開發(fā)人員利用得比較多的便是 String 類的 intern() 方法。 既然運(yùn)行時(shí)常量池是方法區(qū)的一部分趁仙,自然受到方法區(qū)內(nèi)存的限制洪添,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常。

7. 直接內(nèi)存

?? 直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分雀费,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域干奢。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致 OutOfMemoryError 異常出現(xiàn)坐儿,所以我們放到這里一起講解律胀。 在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的 I/O 方式貌矿,它可以使用 Native 函數(shù)庫(kù)直接分配堆外內(nèi)存炭菌,然后通過一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能逛漫,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)黑低。 顯然酌毡,本機(jī)直接內(nèi)存的分配不會(huì)受到 Java 堆大小的限制,但是旷坦,既然是內(nèi)存舌胶,肯定還是會(huì)受到本機(jī)總內(nèi)存(包括 RAM 以及 SWAP 區(qū)或者分頁(yè)文件)大小以及處理器尋址空間的限制婉烟。服務(wù)器管理員在配置虛擬機(jī)參數(shù)時(shí)似袁,會(huì)根據(jù)實(shí)際內(nèi)存設(shè)置 -Xmx 等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級(jí)的限制), 從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn) OutOfMemoryError 異常。

二. 怎么找出“垃圾”

前面說過窑睁,在堆里面存放著 Java 世界中幾乎所有的對(duì)象實(shí)例箫津,垃圾收集器在對(duì)堆進(jìn)行回收前送挑,第一件事情就是要確定這些對(duì)象之中哪些還“存活”著,哪些已經(jīng)“死去”纺裁,這些垃圾對(duì)象可以簡(jiǎn)單的理解為?不可能再被任何途徑使用的對(duì)象?司澎,怎么區(qū)分出這些死亡和存活的對(duì)象呢欺缘,就需要用到垃圾判斷算法谚殊,主要有?引用計(jì)數(shù)法?和?可達(dá)性分析法?剿干。

1. 引用計(jì)數(shù)法

給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí)氢伟,計(jì)數(shù)器值就加1榜轿;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1朵锣;任何時(shí)刻計(jì)數(shù)器為 0 的對(duì)象就是不可能再被使用的谬盐。這些引用計(jì)數(shù)為 0 的對(duì)象,就可以稱之為垃圾猪勇,可以被收集设褐。

微軟公司的 COM(Component Object Model)技術(shù)、使用 ActionScript 3 的 FlashPlayer泣刹、Python 語(yǔ)言和在游戲腳本領(lǐng)域被廣泛應(yīng)用的 Squirrel 中都使用了引用計(jì)數(shù)算法進(jìn)行內(nèi)存管理助析。

優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,判定效率也很高椅您。

缺點(diǎn):需要額外的空間來存儲(chǔ)計(jì)數(shù)器外冀,難以檢測(cè)出對(duì)象之間的循環(huán)引用。

?? 現(xiàn)在主流的 Java 虛擬機(jī)都沒有使用引用計(jì)數(shù)法掀泳,最主要的原因就是它很難解決對(duì)象之間互相循環(huán)引用的問題雪隧。在兩個(gè)對(duì)象循環(huán)引用時(shí)西轩,引用計(jì)數(shù)器都為 1,當(dāng)對(duì)象周期結(jié)束后應(yīng)該被回收卻無法回收脑沿,造成內(nèi)存泄漏藕畔,代碼如下:

publicclassTestReferenceCounter{publicstaticvoidmain(String[]args){TestObject obj1=newTestObject();TestObject obj2=newTestObject();obj1.instance=obj2;obj2.instance=obj1;obj1=null;obj2=null;}staticclassTestObject{Object instance;}}

2. 可達(dá)性分析法(Reachability Analysis)

?? 在主流的商用程序語(yǔ)言(Java、C#庄拇,甚至包括前面提到的古老的 Lisp )的主流實(shí)現(xiàn)中注服,都是通過可達(dá)性分析來判定對(duì)象是否存活的。這個(gè)算法的基本思路就是:

通過一系列的稱為 “GC Roots” 的對(duì)象作為起始點(diǎn)措近,從這些節(jié)點(diǎn)開始向下搜索溶弟;

搜索所走過的路徑稱為引用鏈(Reference Chain);

當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連 (用圖論的話來說瞭郑,就是從 GC Roots 到這個(gè)對(duì)象不可達(dá))時(shí)辜御,則證明此對(duì)象是不可用的。

? ?如下圖所示屈张,對(duì)象object 5擒权、object 6、object 7雖然互相有關(guān)聯(lián)袜茧,但是它們到 GC Roots 是不可達(dá) 的菜拓,所以它們將會(huì)被判定為是可回收的對(duì)象瓣窄。

在 Java 語(yǔ)言中笛厦,可作為 GC Roots 的對(duì)象包括下面幾種:

虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。

方法區(qū)中類靜態(tài)屬性引用的對(duì)象俺夕。

方法區(qū)中常量引用的對(duì)象裳凸。

本地方法棧中 JNI(即一般說的 Native 方法)引用的對(duì)象。

可達(dá)性分析法的優(yōu)缺點(diǎn):

優(yōu)點(diǎn):可以解決循環(huán)引用的問題劝贸,不需要占用額外的空間 ;

缺點(diǎn):實(shí)現(xiàn)比較復(fù)雜姨谷,需要分析大量數(shù)據(jù),消耗大量時(shí)間映九;分析過程需要 GC 停頓(引用關(guān)系不能發(fā)生變化)梦湘,即停頓所有 Java 執(zhí)行線程(稱為 “Stop The World”,是垃圾回收重點(diǎn)關(guān)注的問題)件甥;

3. 引用關(guān)系

?? 無論是通過引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量捌议,還是通過可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá),判定對(duì)象是否存活都與“引用”有關(guān)引有。

3.1. JDK 1.2 以前 Java 的引用定義:

?? 如果 reference 類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址瓣颅,就稱這塊內(nèi)存代表著一個(gè)引用。這種定義太過狹隘譬正,無法描述更多信息宫补。

3.2. JDK 1.2 之后檬姥,Java 對(duì)引用的概念進(jìn)行了擴(kuò)充,分為?強(qiáng)粉怕、軟健民、弱、虛引用贫贝,這 4 種引用強(qiáng)度依次逐漸減弱荞雏。

強(qiáng)引用(Strong Reference)

強(qiáng)引用就是指在程序代碼之中普遍存在的,類似 “Object obj = new Object()” 這類的引用平酿,只要強(qiáng)引用還存在凤优,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象

測(cè)試代碼:

Object object=newObject();System.out.println("Strong Reference object = "+object);System.gc();Thread.sleep(500);System.out.println("Strong Reference gc object = "+object);

運(yùn)行結(jié)果:

Strong Reference object=java.lang.Object@39ba5a14Strong Reference gc object=java.lang.Object@39ba5a14

軟引用(Soft Reference)

軟引用是用來描述一些還有用但并非必需的對(duì)象蜈彼。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象筑辨,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收幸逆。如果這次回收還沒有足夠的內(nèi)存棍辕,才會(huì)拋出內(nèi)存溢出異常。在 JDK 1.2 之后还绘,提供了 SoftReference 類來實(shí)現(xiàn)軟引用楚昭。

測(cè)試代碼:

Object object=newObject();SoftReference<Object>softReference=newSoftReference<>(object);object=null;System.out.println("Soft Reference object = "+softReference.get());System.gc();Thread.sleep(500);System.out.println("Soft Reference gc object = "+softReference.get());

運(yùn)行結(jié)果:

Soft Reference object=java.lang.Object@39ba5a14Soft Reference gc object=java.lang.Object@39ba5a14

弱引用(Weak Reference)

弱引用也是用來描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些拍顷,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前抚太。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠昔案, 都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象尿贫。在 JDK 1.2 之后,提供了 WeakReference 類來實(shí)現(xiàn)弱引用踏揣。

測(cè)試代碼:

WeakReference<Object>weakReference=newWeakReference<>(newObject());System.out.println("Weak Reference object = "+weakReference.get());System.gc();Thread.sleep(500);System.out.println("Weak Reference gc object = "+weakReference.get());

運(yùn)行結(jié)果:

Weak Reference object=java.lang.Object@511baa65Weak Reference gc object=null

虛引用(Phantom Reference)

虛引用也稱為幽靈引用或者幻影引用庆亡,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在捞稿,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響又谋,也無法通過虛引用來取得一個(gè)對(duì)象實(shí)例。

虛引用?主要用來?跟蹤對(duì)象?被垃圾回收器?回收?的活動(dòng)娱局。?虛引用?與?軟引用?和?弱引用?的一個(gè)區(qū)別在于:

虛引用必須和引用隊(duì)列 (ReferenceQueue) 聯(lián)合使用彰亥。當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí),如果發(fā)現(xiàn)它還有虛引用铃辖,就會(huì)?在回收對(duì)象的內(nèi)存之前剩愧,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。

ReferenceQueue<Object>phantomReferenceQueue=newReferenceQueue<>();// 創(chuàng)建虛引用娇斩,要求必須與一個(gè)引用隊(duì)列關(guān)聯(lián)PhantomReference<Object>phantomReference=newPhantomReference<>(newObject(),phantomReferenceQueue);

程序可以通過判斷引用?隊(duì)列?中是否已經(jīng)加入了?虛引用仁卷,來了解被引用的對(duì)象是否將要進(jìn)行?垃圾回收穴翩。如果程序發(fā)現(xiàn)某個(gè)虛引用已經(jīng)被加入到引用隊(duì)列,那么就可以在所引用的對(duì)象的?內(nèi)存被回收之前?采取必要的行動(dòng)锦积。

4. 生存還是死亡

在可達(dá)性分析算法中被標(biāo)記為不可達(dá)的對(duì)象芒帕,也不一定會(huì)被回收,它還有第二次重生的機(jī)會(huì)丰介。每一個(gè)對(duì)象在被回收之前要進(jìn)行兩次標(biāo)記背蟆,一次是?沒有關(guān)聯(lián)引用鏈?會(huì)被標(biāo)記一次,第二次是?判斷該對(duì)象是否覆蓋 finalize()?方法哮幢,如果?沒有覆蓋則真正的被定了“死刑”带膀。

如果這個(gè)對(duì)象被 jvm 判定為有必要執(zhí)行 finalize() 方法,那么這個(gè)對(duì)象會(huì)被放入?F-Queue?隊(duì)列中橙垢,并在稍后由一個(gè)由虛擬機(jī)自動(dòng)創(chuàng)建的垛叨、低優(yōu)先級(jí)的?finalizer?線程去執(zhí)行它。但是這里的“執(zhí)行”是指虛擬機(jī)會(huì)觸發(fā)這個(gè)方法柜某,但是并不代表會(huì)等它運(yùn)行結(jié)束嗽元。虛擬機(jī)在此處是做了優(yōu)化的,因?yàn)槿绻硞€(gè)對(duì)象在?finalize?方法中長(zhǎng)時(shí)間運(yùn)行或者發(fā)生死循環(huán)喂击,將可能導(dǎo)致?F-Queue?隊(duì)列中其他對(duì)象永遠(yuǎn)處于等待剂癌,甚至可能會(huì)導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰。

在 finalize() 方法中我們可以實(shí)現(xiàn)對(duì)這個(gè)對(duì)象的重生翰绊,代碼如下:

publicclassFinalizeEscapeGC{publicstaticFinalizeEscapeGC SAVE_HOOK=null;publicvoidisAlive(){System.out.println("yes,i am still alive");}@Overrideprotectedvoidfinalize()throwsThrowable{super.finalize();System.out.println("finalize method executed佩谷!");FinalizeEscapeGC.SAVE_HOOK=this;}publicstaticvoidmain(String[]args)throwsThrowable{SAVE_HOOK=newFinalizeEscapeGC();// 對(duì)象第一次成功拯救自己SAVE_HOOK=null;System.gc();// 因?yàn)?finalize 方法優(yōu)先級(jí)很低,所以暫停 0.5 秒以等待它Thread.sleep(500);if(SAVE_HOOK!=null){SAVE_HOOK.isAlive();}else{System.out.println("no,i am dead");}//下面這段代碼與上面的完全相同辞做,但是這次自救卻失敗了SAVE_HOOK=null;System.gc();//因?yàn)?finalize 方法優(yōu)先級(jí)很低琳要,所以暫停0.5秒以等待它Thread.sleep(500);if(SAVE_HOOK!=null){SAVE_HOOK.isAlive();}else{System.out.println("no,i am dead");}}}

運(yùn)行結(jié)果:

finalize method executed!yes,i am still aliveno,i am dead

?? 從運(yùn)行結(jié)果可以看到秤茅,第一次拯救成功,第二次拯救失敗童叠,所以需要注意的是 finalize() 方法只會(huì)被系統(tǒng)調(diào)用一次框喳,多次被 gc 只有第一次會(huì)被調(diào)用,因此只有一次的重生機(jī)會(huì)厦坛。

5. 回收方法區(qū)

假如一個(gè)字符串 “abc” 已經(jīng)進(jìn)入了常量池中五垮,但是當(dāng)前系統(tǒng)沒有任何一個(gè) String 對(duì)象是 “abc” ,那么這個(gè)對(duì)象就應(yīng)該回收杜秸。方法區(qū)( HotSpot 虛擬機(jī)中的永久代)的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類放仗。比如上述的 “abc” 就是屬于廢棄常量,那么哪些類是無用的類呢撬碟?

該類所有的實(shí)例都已經(jīng)被回收诞挨,也就是 Java 堆中不存在該類的任何實(shí)例莉撇;

加載該類的 ClassLoader 已經(jīng)被回收;

該類對(duì)應(yīng)的 java.lang.Class 對(duì)象沒有在任何地方被引用惶傻,無法在任何地方通過反射訪問該類的方法棍郎。

三. 垃圾收集算法

?? 通過上面的介紹,我們已經(jīng)知道了什么是垃圾以及如何判斷一個(gè)對(duì)象是否是垃圾银室。那么接下來涂佃,我們就來了解如何回收垃圾,這就是垃圾回收算法和垃圾回收器需要做的事情了蜈敢。

1. 標(biāo)記-清除(Mark-Sweep)算法

最基礎(chǔ)的收集算法是 “標(biāo)記-清除”(Mark-Sweep)算法辜荠,分為 “標(biāo)記” 和 “清除” 兩個(gè)階段:首先標(biāo)記出所需回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象抓狭,它的標(biāo)記過程其實(shí)就是前面的可達(dá)性分析法中判定垃圾對(duì)象的標(biāo)記過程侨拦。

下圖為 “標(biāo)記-清除” 算法的執(zhí)行過程 :

優(yōu)點(diǎn):

不需要進(jìn)行對(duì)象的移動(dòng),并且僅對(duì)不存活的對(duì)象進(jìn)行處理辐宾,在存活對(duì)象比較多的情況下極為高效狱从。

缺點(diǎn):

標(biāo)記和清理的兩個(gè)過程效率都不高;

容易產(chǎn)生內(nèi)存碎片叠纹,碎片空間太多可能導(dǎo)致無法存放大對(duì)象季研。

2. 標(biāo)記-整理(Mark-Compact)算法

?標(biāo)記-整理?算法標(biāo)記的過程與 “標(biāo)記-清除” 算法中的標(biāo)記過程一樣,但對(duì)標(biāo)記后出的垃圾對(duì)象的處理情況有所不同誉察,它不是直接對(duì)可回收對(duì)象進(jìn)行清理与涡,而是讓所有的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存持偏。

下圖為 “標(biāo)記-整理” 算法的示意圖:

優(yōu)點(diǎn):

經(jīng)過整理之后驼卖,新對(duì)象的分配只需要通過指針碰撞便能完成,比較簡(jiǎn)單鸿秆;使用這種方法酌畜,空閑區(qū)域的位置是始終可知的,也不會(huì)再有碎片的問題了卿叽。

缺點(diǎn):

GC 暫停的時(shí)間會(huì)增長(zhǎng)桥胞,因?yàn)槟阈枰獙⑺械膶?duì)象都拷貝到一個(gè)新的地方,還得更新它們的引用地址考婴。

3. 復(fù)制(Copying)算法

?復(fù)制?算法的提出是為了?解決效率問題?和?堆碎片?的垃圾回收贩虾。它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊沥阱。當(dāng)這一塊的內(nèi)存用完了缎罢,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。

下圖為?復(fù)制?算法的示意圖:

優(yōu)點(diǎn):

標(biāo)記階段和復(fù)制階段可以同時(shí)進(jìn)行策精;

每次只對(duì)一塊內(nèi)存進(jìn)行回收舰始,運(yùn)行高效;

只需移動(dòng)棧頂指針蛮寂,按順序分配內(nèi)存即可蔽午,實(shí)現(xiàn)簡(jiǎn)單;

內(nèi)存回收時(shí)不用考慮內(nèi)存碎片的出現(xiàn)酬蹋。

缺點(diǎn):

可用內(nèi)存縮小了一半及老。

?? 復(fù)制算法比較適合于新生代(短生存期的對(duì)象),在老年代(長(zhǎng)生存期的對(duì)象)中范抓,對(duì)象存活率比較高骄恶,如果執(zhí)行較多的復(fù)制操作,效率將會(huì)變低匕垫,所以老年代一般會(huì)選用其他算法僧鲁,如“標(biāo)記-整理”算法。

4. 分代收集(Generational Collector)算法

?分代收集?算法是將堆內(nèi)存劃分為新生代象泵、老年代?和?永久代寞秃,在 jdk8 以后取消了永久代的說法,而是用元空間取代偶惠。新生代又被進(jìn)一步劃分為?Eden?和?Survivor?區(qū)春寿,其中?Survivor?由?FromSpace(Survivor0)和?ToSpace(Survivor1)組成。一般年輕代使用復(fù)制算法(對(duì)象存活率低)忽孽,老年代使用標(biāo)記整理算法(對(duì)象存活率高)绑改。

新生代(Young Generation):

幾乎所有新生成的對(duì)象首先都是放在年輕代的,新生代內(nèi)存按照?8:1:1?的比例分為一個(gè)?Eden?區(qū)和兩個(gè) **Survivor(Survivor0兄一,Survivor1)**區(qū)厘线。新生代對(duì)象生命周期如下:

大部分對(duì)象在?Eden?區(qū)中生成。

當(dāng)新對(duì)象生成出革,Eden?空間申請(qǐng)失斣熳场(因?yàn)榭臻g不足等),則會(huì)發(fā)起一次?GC(Minor GC / Scavenge GC)蹋盆。

回收時(shí)先將?Eden?區(qū)存活對(duì)象復(fù)制到一個(gè)?Survivor0?區(qū)费薄,然后清空?Eden?區(qū)。

當(dāng)這個(gè)?Survivor0?區(qū)也存放滿了時(shí)栖雾,則將?Eden?區(qū)和?Survivor0?區(qū)存活對(duì)象復(fù)制到另一個(gè)?Survivor1?區(qū),然后清空?Eden?和這個(gè)?Survivor0?區(qū)伟众,此時(shí)?Survivor0?區(qū)是空的析藕,然后將?Survivor0?區(qū)和?Survivor1?區(qū)交換,即保持?Survivor1?區(qū)為空凳厢, 如此往復(fù)账胧。

當(dāng)?Survivor1?區(qū)不足以存放?Eden?和?Survivor0?的存活對(duì)象時(shí)竞慢,就將存活對(duì)象直接存放到老年代。

當(dāng)對(duì)象在?Survivor?區(qū)躲過一次?GC?的話治泥,其對(duì)象年齡便會(huì)加?1筹煮。

默認(rèn)情況下,如果對(duì)象年齡達(dá)到?15?歲居夹,就會(huì)移動(dòng)到老年代中败潦。

若是老年代也滿了就會(huì)觸發(fā)一次?Full GC,也就是新生代准脂、老年代都進(jìn)行回收劫扒。

老年代(Old Generation):

?? 在新生代中經(jīng)歷了 N 次垃圾回收后仍然存活的對(duì)象,就會(huì)被放到年老代中狸膏。因此沟饥,可以認(rèn)為年老代中存放的都是一些生命周期較長(zhǎng)的對(duì)象。老年代有以下特點(diǎn):

內(nèi)存比新生代大很多(大概比例是?1:2)湾戳。

作為?Eden?區(qū)的?空間分配擔(dān)保贤旷,當(dāng)?MinorGC?時(shí),如果存活對(duì)象過多砾脑,無法完全放入?Survivor?區(qū)幼驶,就會(huì)向老年代借用內(nèi)存存放對(duì)象。

動(dòng)態(tài)對(duì)象年齡判定拦止,如果?Survivor?區(qū)中相同年齡所有對(duì)象的大小總和大于?Survivor?區(qū)空間一半县遣,年齡大于或者等于該年齡的對(duì)象在?MinorGC?時(shí)將復(fù)制到老年代。

當(dāng)老年代內(nèi)存滿時(shí)觸發(fā)?Major GC?即?Full GC汹族,Full GC?發(fā)生頻率比較低萧求,老年代對(duì)象存活時(shí)間比較長(zhǎng),存活率高顶瞒。

出現(xiàn)了?Major GC?經(jīng)常會(huì)伴隨至少一次?Minor GC(非絕對(duì))夸政,Major GC?的速度一般會(huì)比?Minor GC?慢?10?倍以上。

一般大對(duì)象直接進(jìn)入老年代榴徐。所謂的大對(duì)象是指需要?大量連續(xù)存儲(chǔ)空間?的對(duì)象守问,最常見的一種大對(duì)象就是大數(shù)組。

長(zhǎng)期存活的對(duì)象將進(jìn)入老年代坑资,默認(rèn)為?15?歲耗帕。

對(duì)于晉升老年代的分代年齡閾值,我們可以通過?-XX:MaxTenuringThreshold?參數(shù)進(jìn)行控制袱贮,前面提到晉升老年代的年齡閾值默認(rèn)的 15 歲仿便,為什么不是 16 或者 17 呢?

這里就涉及到對(duì)象的內(nèi)存布局了,在 HotSpot 虛擬機(jī)中嗽仪,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為 3 塊區(qū)域:對(duì)象頭(Header)荒勇、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)


實(shí)際上闻坚,HotSpot 虛擬機(jī)的?對(duì)象頭?其中一部分用于存儲(chǔ)對(duì)象自身的?運(yùn)行時(shí)數(shù)據(jù)沽翔,如哈希碼、GC 分代年齡窿凤、鎖狀態(tài)標(biāo)志仅偎、線程持有的鎖、偏向線程 ID卷玉、偏向時(shí)間戳?等哨颂,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在 32 位和 64 位的虛擬機(jī)(未開啟壓縮指針)中分別為 32bit 和 64bit,官方稱它為?Mark word相种。

在 32 位的 HotSpot 虛擬機(jī)中威恼,如果對(duì)象處于未被鎖定的狀態(tài)下,那么?Mark Word?的 32bit 空間中的 25bit 用于存儲(chǔ)對(duì)象哈希碼寝并,4bit 用于存儲(chǔ)對(duì)象分代年齡箫措,2bit 用于存儲(chǔ)鎖標(biāo)志位,1bit 固定為 0衬潦,如下表所示:


在 64 位系統(tǒng)及 64 位 JVM 下斤蔓,開啟指針壓縮,那么頭部存放 Class 指針的空間大小還是 4 字節(jié)镀岛,而?Mark Word?區(qū)域會(huì)變大弦牡,變成 8 字節(jié),也就是頭部最少為 12 字節(jié)漂羊,如下表所示:


?? 可以看到驾锰,對(duì)象的分代年齡占 4 位,也就是從 0000 到 1111 走越,而其值最大為 15椭豫,所以分代年齡也就不可能超過 15 這個(gè)數(shù)值了。

永久代(Permanent Generation):

?? 用于存放靜態(tài)文件(class 類旨指、方法)和常量等赏酥。永久代對(duì)垃圾回收沒有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些 class谆构,例如 Hibernate 等裸扶,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來存放這些運(yùn)行過程中新增的類。對(duì)永久代的回收主要回收兩部分內(nèi)容:廢棄常量和無用的類搬素。永久代在 Java SE8 特性中已經(jīng)被移除了姓言,取而代之的是元空間(MetaSpace)瞬项,因此也不會(huì)再出現(xiàn)java.lang.OutOfMemoryError: PermGen error的錯(cuò)誤了蔗蹋。

卡表(Card Table)提升 GC 效率

?? 在某些場(chǎng)景下何荚,老年代的對(duì)象可能引用新生代的對(duì)象,那標(biāo)記存活對(duì)象的時(shí)候猪杭,需要掃描老年代中的所有對(duì)象餐塘。因?yàn)樵搶?duì)象擁有對(duì)新生代對(duì)象的引用,那么這個(gè)引用也會(huì)被稱為 GC Roots皂吮,這樣豈不是每次進(jìn)行 Minor GC 時(shí)也要進(jìn)行全堆的掃描戒傻?

HotSpot 給出的解決方案是一項(xiàng)叫做卡表(Card Table)的技術(shù),該技術(shù)將整個(gè)堆劃分為一個(gè)個(gè)大小為 512 字節(jié)的卡蜂筹,并且維護(hù)一個(gè)卡表需纳,用來存儲(chǔ)每張卡的一個(gè)標(biāo)識(shí)位。這個(gè)標(biāo)識(shí)位代表對(duì)應(yīng)的卡是否可能存有指向新生代對(duì)象的引用艺挪。如果可能存在不翩,那么我們就認(rèn)為這張卡是臟的。如下圖:

?? 在進(jìn)行 Minor GC 的時(shí)候麻裳,我們便可以不用掃描整個(gè)老年代口蝠,而是在卡表中尋找臟卡,并將臟卡中的對(duì)象加入到 Minor GC 的 GC Roots 里津坑。當(dāng)完成所有臟卡的掃描之后妙蔗,Java 虛擬機(jī)便會(huì)將所有臟卡的標(biāo)識(shí)位清零〗澹卡表能用于減少老年代的全堆空間掃描眉反,這能很大的提升 GC 效率。

四. 垃圾收集器

?? 如果說收集算法是內(nèi)存回收的方法論穆役,那么垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)寸五。Java 虛擬機(jī)規(guī)范中對(duì)垃圾收集器應(yīng)該如何實(shí)現(xiàn)并沒有任何規(guī)定,因此不同的廠商孵睬、不同版本的虛擬機(jī)所提供的垃圾收集器都可能會(huì)有很大差別播歼,并且一般都會(huì)提供參數(shù)供用戶根據(jù)自己的應(yīng)用特點(diǎn)和要求組合出各個(gè)年代所使用的收集器。這里討論的收集器基于JDK 1.7 Update 14之后的 HotSpot 虛擬機(jī)(在此版本中正式提供了商用的G1收集器掰读,之前G1仍處于實(shí)驗(yàn)狀態(tài)) 秘狞,這個(gè)虛擬機(jī)包含的所有收集器如圖所示。

圖中展示了 7 種作用于不同分代的收集器蹈集,如果兩個(gè)收集器之間存在連線烁试,就說明它們可以搭配使用。虛擬機(jī)所處的區(qū)域拢肆,則表示它是屬于新生代收集器還是老年代收集器减响。

1. Serial 收集器

?? Serial 收集器是最基本靖诗、發(fā)展歷史最悠久的收集器,看名字就會(huì)知道支示,這個(gè)收集器是一個(gè)單線程的收集器刊橘,Serial 收集器采用單線程方式進(jìn)行收集,且在 GC 線程工作時(shí)颂鸿,系統(tǒng)不允許應(yīng)用線程打擾促绵。此時(shí),應(yīng)用程序進(jìn)入暫停狀態(tài)嘴纺,即 Stop-the-world褐墅。Stop-the-world 暫停時(shí)間的長(zhǎng)短泡躯,是衡量一款收集器性能高低的重要指標(biāo)。

回收區(qū)域:新生代;

采用算法:復(fù)制算法持际;

運(yùn)行環(huán)境:運(yùn)行在 Client 模式下的默認(rèn)新生代收集器儡羔。

2. ParNew 收集器

?? ParNew 收集器其實(shí)就是 Serial 收集器的多線程版本冯勉,除了使用多條線程進(jìn)行垃圾收集之外纷妆,其余行為包括 Serial 收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold佛致、-XX:HandlePromotionFailure 等)贮缕、收集算法、Stop The World俺榆、對(duì)象分配規(guī)則感昼、回收策略等都與Serial收集器完全一樣,在實(shí)現(xiàn)上罐脊,這兩種收集器也共用了相當(dāng)多的代碼定嗓。ParNew 收集器的工作過程如圖所示:

3. Parallel Scavenge 收集器

?? Parallel Scavenge 是針對(duì)新生代的垃圾回收器,采用“復(fù)制”算法萍桌,和 ParNew 類似宵溅,但更注重吞吐率。在 ParNew 的基礎(chǔ)上演化而來的 Parallel Scanvenge 收集器被譽(yù)為“吞吐量?jī)?yōu)先”收集器上炎。吞吐量就是 CPU 用于運(yùn)行用戶代碼的時(shí)間與 CPU 總消耗時(shí)間的比值恃逻,即吞吐量 = 運(yùn)行用戶代碼時(shí)間 /(運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)。如虛擬機(jī)總運(yùn)行了 100 分鐘藕施,其中垃圾收集花掉 1 分鐘寇损,那吞吐量就是99%。

?? Parallel Scanvenge 收集器在 ParNew 的基礎(chǔ)上提供了一組參數(shù)裳食,用于配置期望的收集時(shí)間或吞吐量矛市,然后以此為目標(biāo)進(jìn)行收集。通過 VM 選項(xiàng)可以控制吞吐量的大致范圍:

-XX:MaxGCPauseMills:期望收集時(shí)間上限诲祸,用來控制收集對(duì)應(yīng)用程序停頓的影響浊吏。

-XX:GCTimeRatio:期望的 GC 時(shí)間占總時(shí)間的比例而昨,用來控制吞吐量。

-XX:UseAdaptiveSizePolicy:自動(dòng)分代大小調(diào)節(jié)策略找田。

?? 但要注意停頓時(shí)間與吞吐量這兩個(gè)目標(biāo)是相悖的歌憨,降低停頓時(shí)間的同時(shí)也會(huì)引起吞吐的降低。因此需要將目標(biāo)控制在一個(gè)合理的范圍中午阵。

4. Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本躺孝,單線程收集器,采用“標(biāo)記-整理”算法底桂。這個(gè)收集器的主要意義也是在于給 Client 模式下的虛擬機(jī)使用。

5. Parallel Old 收集器

?? Parallel Old 是 Parallel Scanvenge 收集器的老年代版本惧眠,多線程收集器籽懦,采用“標(biāo)記-整理”算法。這個(gè)收集器是在 JDK 1.6 中才開始提供的氛魁,在此之前暮顺,新生代的 Parallel Scavenge 收集器一直處于比較尷尬的狀態(tài)。原因是秀存,如果新生代選擇了 Parallel Scavenge 收集器捶码,老年代除了 Serial Old(PS MarkSweep)收集器外別無選擇(Parallel Scavenge 收集器無法與CMS收集器配合工作)。由于老年代 Serial Old 收集器在服務(wù)端應(yīng)用性能上的“拖累”或链,使用了 Parallel Scavenge 收集器也未必能在整體應(yīng)用上獲得吞吐量最大化的效果惫恼,由于單線程的老年代收集中無法充分利用服務(wù)器多 CPU 的處理能力,在老年代很大而且硬件比較高級(jí)的環(huán)境中澳盐,這種組合的吞吐量甚至還不一定有 ParNew 加 CMS 的組合“給力”祈纯。

直到Parallel Old收集器出現(xiàn)后,“吞吐量?jī)?yōu)先”收集器終于有了比較名副其實(shí)的應(yīng)用組合叼耙,在注重吞吐量以及CPU資源敏感的場(chǎng)合腕窥,都可以優(yōu)先考慮 Parallel Scavenge 加 Parallel Old 收集器。Parallel Old 收集器的工作過程如圖所示:

6. CMS收集器

?? CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器筛婉。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)站或者 B/S 系統(tǒng)的服務(wù)端上簇爆,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短爽撒,以給用戶帶來較好的體驗(yàn)入蛆。CMS 收集器就非常符合這類應(yīng)用的需求。

?? 從名字(包含 “Mark Sweep” )上就可以看出匆浙,CMS 收集器是基于“標(biāo)記—清除”算法實(shí)現(xiàn)的安寺,它的運(yùn)作過程相對(duì)于前面幾種收集器來說更復(fù)雜一些,整個(gè)過程分為 4 個(gè)步驟首尼,包括:

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

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

重新標(biāo)記(CMS remark)

并發(fā)清除(CMS concurrent sweep)

?? 其中挑庶,初始標(biāo)記言秸、重新標(biāo)記這兩個(gè)步驟仍然需要 “Stop The World”。初始標(biāo)記僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象迎捺,速度很快举畸,并發(fā)標(biāo)記階段就是進(jìn)行 GC RootsTracing 的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄凳枝,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些抄沮,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。

?? 由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作岖瑰,所以叛买,從總體上來說,CMS 收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的蹋订。通過下圖可以比較清楚地看到 CMS 收集器的運(yùn)作步驟中并發(fā)和需要停頓的時(shí)間率挣。

優(yōu)點(diǎn):

并發(fā)收集,低停頓露戒。

缺點(diǎn):

CPU 資源非常敏感椒功;

無法處理浮動(dòng)垃圾;

是基于“標(biāo)記-清除”算法智什,該算法的缺點(diǎn)都有动漾。

?? CMS 收集器之所以能夠做到并發(fā),根本原因在于采用基于“標(biāo)記-清除”的算法并對(duì)算法過程進(jìn)行了細(xì)粒度的分解荠锭。前面已經(jīng)介紹過“標(biāo)記-清除”算法將產(chǎn)生大量的內(nèi)存碎片這對(duì)新生代來說是難以接受的旱眯,因此新生代的收集器并未提供 CMS 版本。

7. G1 收集器

?? G1(Garbage-First)收集器是當(dāng)今收集器技術(shù)發(fā)展最前沿的成果之一节沦,它是一款 面向服務(wù)端應(yīng)用的垃圾收集器键思,HotSpot 開發(fā)團(tuán)隊(duì)賦予它的使命是(在比較長(zhǎng)期的)未來可以替換掉 JDK 1.5 中發(fā)布的 CMS 收集器。與其他 GC 收集器相比甫贯,G1 具備如下特點(diǎn):

并行與并發(fā):G1 能夠重發(fā)利用多 CPU吼鳞、多核環(huán)境下的優(yōu)勢(shì),使用多個(gè) CPU 來縮短 Stop-The-World 停頓時(shí)間叫搁。

分代收集:與其他收集器一樣赔桌,分代概念在 G1 中依然存在。雖然 G1 可以不需要其他收集器配合就能獨(dú)立管理整個(gè) GC 堆渴逻,但它能夠采用不同方式去處理新創(chuàng)建的對(duì)象和已存活一段時(shí)間疾党、熬過多次 GC 的舊對(duì)象來獲取更好的收集效果。

空間整合:與 CMS 的“標(biāo)記-清理”算法不同惨奕,G1 從整體來看是基于“標(biāo)記-整理”來實(shí)現(xiàn)的收集器雪位,從局部(兩個(gè) Region 之間)上來看是基于“復(fù)制”算法實(shí)現(xiàn)的,這兩種算法都意味著 G1 運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片梨撞,收集后能夠提供整體的可用內(nèi)存雹洗。

可預(yù)測(cè)停頓:G1 除了追求低停頓之外香罐,還能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi)时肿,消耗在垃圾收集上的時(shí)間不得超過N毫秒庇茫。

橫跨整個(gè)堆內(nèi)存

使用G1收集器時(shí),Java堆的內(nèi)存布局與其他收集器有很大的區(qū)別螃成,它將?整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region)旦签,雖然還保留著新生代和老年代的概念,但?新生代和老年代不再是物理隔離的了寸宏,他們都是一部分Region(不需要連續(xù))的集合宁炫。

建立可預(yù)測(cè)的時(shí)間模型

G1 收集器之所以能夠建立可預(yù)測(cè)的停頓時(shí)間模型,是因?yàn)樗梢?有計(jì)劃地避免在整個(gè)Java對(duì)中進(jìn)行全區(qū)域的垃圾收集击吱。G1 跟蹤各個(gè) Region 里面垃圾堆積的價(jià)值大辛艿怼(回收所獲得的空間大小以及回收所需要時(shí)間的經(jīng)驗(yàn)值),在后臺(tái)維護(hù)一個(gè)優(yōu)先列表覆醇,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的 Region(這也是 Garbage-First 名稱的由來)炭臭。G1 收集的運(yùn)作過程大致如下:

初始標(biāo)記(Initial Marking):僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象永脓,并且修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發(fā)運(yùn)行時(shí)鞋仍,能在正確可用的 Region 中創(chuàng)建新對(duì)象常摧,這階段需要停頓線程,但耗時(shí)很短威创。

并發(fā)標(biāo)記(Concurrent Marking):是從GC Roots開始堆中對(duì)象進(jìn)行可達(dá)性分析落午,找出存活的對(duì)象,這階段耗時(shí)較長(zhǎng)肚豺,但可與用戶程序并發(fā)執(zhí)行溃斋。

最終標(biāo)記(Final Marking):是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分標(biāo)記記錄,虛擬機(jī)將這段時(shí)間對(duì)象變化記錄在線程 Remembered Set Logs 里面吸申,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到 Remembered Set 中梗劫,這階段需要停頓線程,但是可并行執(zhí)行截碴。

篩選回收(Live Data Counting and Evacuation):首先對(duì)各個(gè) Region 的回收價(jià)值和成本進(jìn)行排序梳侨,根據(jù)用戶所期望的 GC 停頓時(shí)間來制定回收計(jì)劃。這個(gè)階段也可以做到與用戶程序一起并發(fā)執(zhí)行日丹,但是因?yàn)橹换厥找徊糠?Region走哺,時(shí)間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率哲虾。

通過下圖可以比較清楚地看到 G1 收集器的運(yùn)作步驟中并發(fā)和需要停頓的階段(Safepoint處):

G1 的 GC 模式可以分為兩種丙躏,分別為:

Young GC:?在分配一般對(duì)象(非巨型對(duì)象)時(shí)择示,當(dāng)所有 Eden 區(qū)域使用達(dá)到最大閥值并且無法申請(qǐng)足夠內(nèi)存時(shí),會(huì)觸發(fā)一次 YoungGC彼哼。每次 Young GC 會(huì)回收所有 Eden 以及 Survivor 區(qū)对妄,并且將存活對(duì)象復(fù)制到 Old 區(qū)以及另一部分的 Survivor 區(qū)。

Mixed GC:?當(dāng)越來越多的對(duì)象晉升到老年代時(shí)敢朱,為了避免堆內(nèi)存被耗盡剪菱,虛擬機(jī)會(huì)觸發(fā)一個(gè)混合的垃圾收集器,即 Mixed GC拴签,該算法并不是一個(gè) Old GC孝常,除了回收整個(gè)新生代,還會(huì)回收一部分的老年代蚓哩,這里需要注意:是一部分老年代构灸,而不是全部老年代,可以選擇哪些 Old 區(qū)域進(jìn)行收集岸梨,從而可以對(duì)垃圾回收的耗時(shí)時(shí)間進(jìn)行控制喜颁。G1 沒有 Full GC 概念,需要 Full GC 時(shí)曹阔,調(diào)用 Serial Old GC 進(jìn)行全堆掃描半开。

總結(jié):

查看 JVM 使用的默認(rèn)垃圾收集器,可以在 Mac 終端或者 Windows 的 CMD 執(zhí)行如下命令:

java-XX:+PrintCommandLineFlags-version

以我的電腦為例赃份,執(zhí)行結(jié)果為:

-XX:G1ConcRefinementThreads=10-XX:GCDrainStackTargetSize=64-XX:InitialHeapSize=267050880-XX:MaxHeapSize=4272814080-XX:+PrintCommandLineFlags-XX:ReservedCodeCacheSize=251658240-XX:+SegmentedCodeCache-XX:+UseCompressedClassPointers-XX:+UseCompressedOops-XX:+UseG1GC-XX:-UseLargePagesIndividualAllocationjava version"12.0.1"2019-04-16Java(TM)SE Runtime Environment(build12.0.1+12)JavaHotSpot(TM)64-Bit Server VM(build12.0.1+12,mixed mode,sharing)

垃圾收集器參數(shù)總結(jié)

UseSerialGC:?虛擬機(jī)運(yùn)行在 Client 模式下的默認(rèn)值寂拆,打開次開關(guān)后,使用 Serial + Serial Old 的收集器組合進(jìn)行內(nèi)存回收抓韩。

UseParNewGC:?打開次開關(guān)后纠永,使用 ParNew + Serial Old 的收集器組合進(jìn)行內(nèi)存回收。

UseConcMarkSweepGC:?打開次開關(guān)后谒拴,使用ParNew + CMS + Serial Old的收集器組合進(jìn)行內(nèi)存回收尝江,Serial Old 收集器將作為 CMS 收集器出現(xiàn) Concurrent Mode Failure 失敗后的備用收集器使用。

UseParallelGC:?虛擬機(jī)運(yùn)行在 Server 模式下的默認(rèn)值彪薛,打開此開關(guān)后茂装,使用 Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進(jìn)行內(nèi)存回收。

UseParallelOldGC:?打開此開關(guān)后善延,使用 Parallel Scavenge + Parallel Old 的收集器組合進(jìn)行內(nèi)存回收少态。

SurvivorRatio:?新生代中 Eden 區(qū)域與 Survivor 區(qū)域的容量比值,默認(rèn)為 8 易遣,代表 Eden :Survivor = 8 : 1彼妻。

PretenureSizeThreshold:?直接晉升到老年代的對(duì)象大小,設(shè)置這個(gè)參數(shù)后,大于這個(gè)參數(shù)的對(duì)象將直接在老年代分配侨歉。

MaxTenuringThreshold:?晉升到老年代的對(duì)象年齡屋摇。每個(gè)對(duì)象在堅(jiān)持過一次 Minor GC 后,年齡就增加 1 幽邓,當(dāng)超過這個(gè)參數(shù)值時(shí)就進(jìn)入老年代炮温。

UseAdaptiveSizePolicy:?動(dòng)態(tài)調(diào)整 Java 堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡。

HandlePromotionFailure:?是否允許分配擔(dān)保失敗牵舵,即老年代的剩余空間不足以應(yīng)付新生代的整個(gè) Eden 和 Survivor 區(qū)的所有對(duì)象都存活的極端情況柒啤。

ParallelGCThreads:?設(shè)置并行 GC 時(shí)進(jìn)行內(nèi)存回收的線程數(shù)。

GCTimeRatio:?GC 時(shí)間占總時(shí)間的比例畸颅,默認(rèn)值為99担巩,即允許 1% 的 GC 時(shí)間。僅在使用 Parallel Scavenge 收集器時(shí)生效没炒。

MaxGCPauseMillis:?設(shè)置 GC 的最大停頓時(shí)間涛癌。僅在使用 Parallel Scavenge 收集器時(shí)生效。

CMSInitiatingOccupancyFraction:?設(shè)置 CMS 收集器在老年代空間被使用多少后觸發(fā)垃圾收集送火。默認(rèn)值為 68%拳话,僅在使用 CMS 收集器時(shí)生效。

UseCMSCompactAtFullCollection:?設(shè)置 CMS 收集器在完成垃圾收集后是否要進(jìn)行一次內(nèi)存碎片整理种吸。僅在使用 CMS 收集器時(shí)生效假颇。

CMSFullGCsBeforeCompaction:?設(shè)置 CMS 收集器在進(jìn)行若干次垃圾收集后再啟動(dòng)一次內(nèi)存碎片整理。僅在使用 CMS 收集器時(shí)生效骨稿。

參考資料:

《深入理解 Java 虛擬機(jī)》 周志明 著

JVM 垃圾回收機(jī)制(GC)總結(jié)

Java 虛擬機(jī)(JVM)你只要看這一篇就夠了!

JVM-深入分析對(duì)象的內(nèi)存布局

jvm-卡表姜钳,垃圾回收時(shí)的重要手段

深入理解JVM坦冠,7種垃圾收集器

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市哥桥,隨后出現(xiàn)的幾起案子辙浑,更是在濱河造成了極大的恐慌,老刑警劉巖拟糕,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件判呕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡送滞,警方通過查閱死者的電腦和手機(jī)侠草,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來犁嗅,“玉大人边涕,你說我怎么就攤上這事。” “怎么了功蜓?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵园爷,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我式撼,道長(zhǎng)童社,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任著隆,我火速辦了婚禮扰楼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘旅东。我一直安慰自己灭抑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布抵代。 她就那樣靜靜地躺著腾节,像睡著了一般。 火紅的嫁衣襯著肌膚如雪荤牍。 梳的紋絲不亂的頭發(fā)上案腺,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音康吵,去河邊找鬼劈榨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晦嵌,可吹牛的內(nèi)容都是我干的同辣。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼惭载,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼旱函!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起描滔,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤棒妨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后含长,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體券腔,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年拘泞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纷纫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡田弥,死狀恐怖涛酗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤商叹,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布燕刻,位于F島的核電站,受9級(jí)特大地震影響剖笙,放射性物質(zhì)發(fā)生泄漏卵洗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一弥咪、第九天 我趴在偏房一處隱蔽的房頂上張望过蹂。 院中可真熱鬧,春花似錦聚至、人聲如沸酷勺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)脆诉。三九已至,卻和暖如春贷币,著一層夾襖步出監(jiān)牢的瞬間击胜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工役纹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留偶摔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓促脉,卻偏偏與公主長(zhǎng)得像辰斋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瘸味,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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