Java虛擬機(jī)系列——檢視閱讀
參考
Class類文件講解不夠透徹,需要找份新的資料。
內(nèi)存區(qū)域
Java虛擬機(jī)在執(zhí)行Java程序過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)(主要5個(gè)部分)不同的數(shù)據(jù)區(qū)域。這些區(qū)域有自各的用途,以及創(chuàng)建及銷毀時(shí)間,有的區(qū)域(方法區(qū)、堆、直接內(nèi)存、java代碼緩存)隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在,有些區(qū)域(虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器)則是依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。根據(jù)《Java虛擬機(jī)規(guī)范(第2版)》規(guī)定,Java虛擬機(jī)管理的內(nèi)存區(qū)域包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域,下如圖

1.程序計(jì)數(shù)器(Program Counter Register)
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它的作用可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器工作時(shí)就是通過該計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
由于Java虛擬機(jī)的多線程是通過線程輪流切換CPU時(shí)間片的方式來實(shí)現(xiàn)的,所以在任何一個(gè)時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來說是一個(gè)內(nèi)核)只會(huì)行一條線程中的指令。因此為了線程切換后能夠恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,為線程所私有。
如果當(dāng)前線程執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器指向在執(zhí)行的虛擬機(jī)字節(jié)碼的地址;如果執(zhí)行的是一個(gè)Native方法,這個(gè)計(jì)數(shù)器的值為空(UndefinedD),計(jì)數(shù)器必須要能容納方法的返回地址或者具體平臺(tái)的本地指針。此區(qū)域是唯一一個(gè)在Java虛擬機(jī)器中沒有規(guī)定任何OutOfMemoryError的區(qū)域。
2.Java虛擬機(jī)棧
與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型;每個(gè)方法被執(zhí)行時(shí)都會(huì)在虛擬機(jī)棧中創(chuàng)建一個(gè)棧楨(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法返回地址這四種信息。每一個(gè)方法被調(diào)用直至其執(zhí)行完成就像是一個(gè)棧楨在虛擬機(jī)棧中入棧與出棧的過程。
虛擬機(jī)規(guī)范中說明了,Java虛擬機(jī)??梢员粚?shí)現(xiàn)為固定大小,或者根據(jù)計(jì)算的需要?jiǎng)討B(tài)擴(kuò)展和收縮。如果Java虛擬機(jī)棧的大小是固定的,則可以在創(chuàng)建該虛擬機(jī)棧時(shí)獨(dú)立地選擇每個(gè)Java虛擬機(jī)堆棧的大小。Java虛擬機(jī)實(shí)現(xiàn)可以為程序員或用戶提供對(duì)Java虛擬機(jī)棧初始大小的控制,在動(dòng)態(tài)擴(kuò)展或收縮Java虛擬機(jī)堆棧的情況下,還可以提供對(duì)最大和最小大小的控制。
虛擬機(jī)規(guī)范在這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求棧深度超過虛擬機(jī)允許的深度,虛擬機(jī)將會(huì)拋出一個(gè)StackOverflowError錯(cuò)誤;如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展(當(dāng)前大部分虛擬機(jī)都可以動(dòng)態(tài)擴(kuò)展,只不過Java虛擬機(jī)規(guī)范允許固定長(zhǎng)度的虛擬機(jī)棧),當(dāng)擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存或者在創(chuàng)建一條新的線程時(shí)沒有足夠的內(nèi)存創(chuàng)建一個(gè)初始大小的虛擬機(jī)棧時(shí),Java虛擬機(jī)將拋出OutOfMemoryError錯(cuò)誤。
3.本地方法棧
本地方法棧(Native Method Stacks)與虛擬機(jī)棧的作用非常相似,其區(qū)別不過是虛擬機(jī)棧執(zhí)行的是Java方法,而本地方法棧執(zhí)行的是Native方法。虛擬機(jī)規(guī)范中對(duì)本地方法棧中的方法使用的語言,使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,具體的虛擬機(jī)可以自由實(shí)現(xiàn)它。甚至的有虛擬機(jī)(如Sun HotSpot)直接把本地方法棧與虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧區(qū)也會(huì)拋出StackOverflowError與OutOfMemoryError錯(cuò)誤。
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)存。虛擬機(jī)規(guī)范中的描述是:所有類的實(shí)例與數(shù)組對(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í)現(xiàn)成動(dòng)態(tài)擴(kuò)展與收縮的,不過當(dāng)前主流的虛擬機(jī)都是可以進(jìn)行動(dòng)態(tài)擴(kuò)展與收縮的(通過-Xmx與-Xms控制)。如果在堆中沒有足夠的內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemory錯(cuò)誤。
5.方法區(qū)
方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。在虛擬機(jī)啟動(dòng)的時(shí)候方法區(qū)被創(chuàng)建。
對(duì)于HotSpot虛擬機(jī),方法區(qū)又被稱為"永久代(Permanent Generation)",本質(zhì)上兩者并不等價(jià),僅僅是因?yàn)镠otSpot虛擬機(jī)把GC分代收集擴(kuò)展到了方法區(qū),或者說永久來實(shí)現(xiàn)方法區(qū)而已。對(duì)于其它虛擬機(jī)(如BEA JRockit, IMB J9)來說是不存在永久代的概念的,就是HotSpot現(xiàn)在也有放棄永久代并"搬家"至Native Memory來實(shí)現(xiàn)方法區(qū)的規(guī)劃了(在JDK8中已經(jīng)去除了永久代,JDK7中就開始將一些原本存儲(chǔ)在方法區(qū)中的數(shù)據(jù)移至Java堆中:運(yùn)行時(shí)常量池)。
與Java堆一樣,方法區(qū)不需要連續(xù)的內(nèi)存和可以選擇固定大小與可擴(kuò)展收縮外,還可以選擇不實(shí)現(xiàn)垃圾收集,因?yàn)榉椒▍^(qū)的垃圾收集效果不理想。當(dāng)方法區(qū)無法滿足內(nèi)在分配要求時(shí),將拋出OutOfMemory異常。
6.運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Contant Pool)是方法區(qū)的一部分。class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量表(Constant Pool Table),用于存儲(chǔ)編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
運(yùn)行時(shí)常量池相對(duì)于class文件常量池的一個(gè)重要特征就是具備動(dòng)態(tài)性,Java語言并不要求常量一定只能在編譯期產(chǎn)生,也就是并非預(yù)置入class方法中的常量池中的內(nèi)容才能進(jìn)入到方法區(qū)的運(yùn)行時(shí)常量池,程序運(yùn)行期間也可以將新的常量放入常量池中,例如String類的intern()方法。
運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然也會(huì)受到方法區(qū)內(nèi)存大小的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemory異常。
7.直接內(nèi)存——堆外內(nèi)存,受服務(wù)器實(shí)際內(nèi)存大小限制
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError錯(cuò)誤出現(xiàn)。垃圾進(jìn)行收集時(shí),虛擬機(jī)雖然會(huì)對(duì)直接內(nèi)存進(jìn)行回收,但卻不能像新生代與老年代那樣,發(fā)現(xiàn)空間不足了就通知收集器進(jìn)行垃圾回收,它只能等到老年代滿了后FullGC時(shí),然后"順便"清理掉直接內(nèi)存中廢棄的對(duì)象。
在JDK1.4中新加入了NIO,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方法,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆里面的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。
疑問:
Q: 虛擬機(jī)規(guī)范中說明了,Java虛擬機(jī)??梢员粚?shí)現(xiàn)為固定大小,或者根據(jù)計(jì)算的需要?jiǎng)討B(tài)擴(kuò)展和收縮。如果Java虛擬機(jī)棧的大小是固定的,則可以在創(chuàng)建該虛擬機(jī)棧時(shí)獨(dú)立地選擇每個(gè)Java虛擬機(jī)堆棧的大小。怎么實(shí)現(xiàn)呢?
Q: 此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。虛擬機(jī)規(guī)范中的描述是:所有類的實(shí)例與數(shù)組對(duì)象都要在堆中分配。幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存,還有的是在運(yùn)行時(shí)常量池里的是么?靜態(tài)變量和靜態(tài)實(shí)例在這里存放對(duì)象實(shí)例。
Q:如果從內(nèi)存分配的角度看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)。什么叫線程私有的分配緩沖區(qū)TLAB?
Q:方法區(qū)可以選擇不實(shí)現(xiàn)垃圾收集,具體命令是什么?
判斷對(duì)象是否存活
堆中幾乎存放著Java世界中所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆回收之前,第一件事情就是要確定這些對(duì)象哪些還“存活”著,哪些對(duì)象已經(jīng)“死去”(即不可能再被任何途徑使用的對(duì)象)
1.引用計(jì)數(shù)算法——Reference Counting
很多教科書判斷對(duì)象是否存活的算法是這樣的:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值加1;當(dāng)引用失效時(shí),計(jì)數(shù)器減1;任何時(shí)刻計(jì)數(shù)器都為0的對(duì)象就是不可能再被使用的。
引用計(jì)數(shù)算法(Reference Counting)的實(shí)現(xiàn)簡(jiǎn)單,判斷效率也很高,在大部分情況下它都是一個(gè)不錯(cuò)的算法。但是Java語言中沒有選用引用計(jì)數(shù)算法來管理內(nèi)存,其中最主要的一個(gè)原因是它很難解決對(duì)象之間相互循環(huán)引用的問題。
例如:在testGC()方法中,對(duì)象objA和objB都有字段instance,賦值令objA.instance=objB及objB.instance=objA,除此之外這兩個(gè)對(duì)象再無任何引用,實(shí)際上這兩個(gè)對(duì)象都已經(jīng)不能再被訪問,但是它們因?yàn)橄嗷ヒ弥鴮?duì)象方,因此它們的引用計(jì)數(shù)都不為0,于是引用計(jì)數(shù)算法無法通知GC收集器回收它們。
<pre class="md-fences md-end-block" lang="java" contenteditable="false" cid="n106" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 1024;
/*
- 只是為了占點(diǎn)內(nèi)存
*/
private byte[] bigSize = new byte[2 * _1MB];
?
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
?
objA = null;
objB = null;
?
//假設(shè)在這里發(fā)生GC,那么objA與objB是否會(huì)被回收
System.gc();
?
?
?
}
}</pre>
運(yùn)行結(jié)果為:[Tenured: 4237K->141K(6148K), 0.0052656 secs] 4237K->141K(7108K)
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n109" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
[GC [DefNew: 234K->64K(960K), 0.0009447 secs][Tenured: 2125K->2189K(4096K), 0.0048757 secs] 2282K->2189K(5056K), [Perm : 365K->365K(12288K)], 0.0058659 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System) [Tenured: 4237K->141K(6148K), 0.0052656 secs] 4237K->141K(7108K), [Perm : 365K->365K(12288K)], 0.0052973 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
def new generation total 960K, used 18K [0x23b10000, 0x23c10000, 0x23ff0000)
eden space 896K, 2% used [0x23b10000, 0x23b14818, 0x23bf0000)
from space 64K, 0% used [0x23c00000, 0x23c00000, 0x23c10000)
to space 64K, 0% used [0x23bf0000, 0x23bf0000, 0x23c00000)
tenured generation total 6148K, used 141K [0x23ff0000, 0x245f1000, 0x27b10000)
the space 6148K, 2% used [0x23ff0000, 0x240136b8, 0x24013800, 0x245f1000)
compacting perm gen total 12288K, used 365K [0x27b10000, 0x28710000, 0x2bb10000)
the space 12288K, 2% used [0x27b10000, 0x27b6b578, 0x27b6b600, 0x28710000)
ro space 8192K, 63% used [0x2bb10000, 0x2c023b48, 0x2c023c00, 0x2c310000)
rw space 12288K, 53% used [0x2c310000, 0x2c977f38, 0x2c978000, 0x2cf10000)</pre>
在運(yùn)行結(jié)果中可以看到GC日志中包含"4237K->141K",老年代從4273K(大約4M,其實(shí)就是objA與objB)變?yōu)榱?41K,意味著虛擬并沒有因?yàn)檫@兩個(gè)對(duì)象相互引用就不回收它們,這也證明虛擬機(jī)并不是通過通過引用計(jì)數(shù)算法來判斷對(duì)象是否存活的。大家可以看到對(duì)象進(jìn)入了老年代,但是大家都知道,對(duì)象剛創(chuàng)建的時(shí)候是分配在新生代中的,要進(jìn)入老年代默認(rèn)年齡要到了15才行,但這里objA與objB卻進(jìn)入了老年代。這是因?yàn)镴ava堆區(qū)會(huì)動(dòng)態(tài)增長(zhǎng),剛開始時(shí)堆區(qū)較小,對(duì)象進(jìn)入老年代還有一規(guī)則,當(dāng)Survior空間中同一代的對(duì)象大小之和超過Survior空間的一半時(shí),對(duì)象將直接進(jìn)行老年代。
2.根搜索算法——GC Roots Tracing——可達(dá)性分析法
根搜索算法(GC Roots Tracing)判斷對(duì)象是否存活的,也叫可達(dá)性分析法,可達(dá)性指的是該對(duì)象到GC Roots是否有引用鏈可達(dá),不可達(dá)則表示該對(duì)象是可以被回收的。
在主流的商用程序語言中(Java和C#),都是使用根搜索算法(GC Roots Tracing)判斷對(duì)象是否存活的。這個(gè)算法的基本思路就是通過一系列名為"GC Roots"的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的,如下圖:

上圖中Object1、Object2、Object3、Object4到GC Roots是可達(dá)的,表示它們是有引用的對(duì)象,是存活的對(duì)象不可以進(jìn)行回收;Object5、Object6、Object7雖然是互相關(guān)聯(lián)的,但是它們到GC Roots是不可達(dá)的,所以他們是可以進(jìn)行回收的對(duì)象。 在Java語言里,可作為GC Roots對(duì)象的包括如下4種:
虛擬機(jī)棧(棧楨中的局部變量表)中的引用的對(duì)象
方法區(qū)中的類靜態(tài)屬性引用的對(duì)象
方法區(qū)中的常量引用的對(duì)象
本地方法棧中JNI的引用的對(duì)象(Java Native Interface : Java本地接口 )
疑問:
Q:新生代進(jìn)入老年代的情況都有哪些?窮舉出來。
A: 有以下幾種情況:
新生代存活年齡達(dá)到15
大對(duì)象數(shù)組直接進(jìn)入老年代
當(dāng)Survior空間中同一代的對(duì)象大小之和超過Survior空間的一半時(shí),對(duì)象將直接進(jìn)行老年代。
Minor GC后存活的對(duì)象總的大小超過Survior空間直接進(jìn)入老年代。
其他
Q: 類靜態(tài)屬性引用的對(duì)象為什么是在方法區(qū)中,是存放在方法區(qū)中的運(yùn)行時(shí)常量池中么?如果是JDK1.8的話,那么運(yùn)行時(shí)常量池就是在堆中了,就應(yīng)該說是堆中的類靜態(tài)屬性引用的對(duì)象?
Q: 方法區(qū)中的常量引用的對(duì)象是指static修飾的類字段么?方法區(qū)中的類靜態(tài)屬性引用的對(duì)象指的是static final 修飾的類字段么?什么叫常量,什么叫類靜態(tài)屬性?為什么GC Roots對(duì)象強(qiáng)調(diào)的是方法區(qū)中常量和類靜態(tài)屬性引用的對(duì)象呢?而不是堆中的?
A:不是,方法區(qū)中的常量引用的對(duì)象是指final修飾的類字段,方法區(qū)中的類靜態(tài)屬性引用的對(duì)象指的是static修飾的類字段。
常量表示不可變的變量,這種也叫常量,從語法上來講也就是,加上final,使用final關(guān)鍵字來修飾某個(gè)變量,然后只要賦值之后,就不能改變了,就不能再次被賦值了。
垃圾收集算法——回收垃圾算法
當(dāng)對(duì)象判定為"已死"狀態(tài),虛擬就要采取一定的手段將這些對(duì)象從內(nèi)存中移除,即回收垃圾,回收過程有采用一定的算法。如下是一些主要的垃圾收集算法:
1.標(biāo)記-清除算法——Mark-Sweep
該算法是最基礎(chǔ)的算法,分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象。之所有說它是最基礎(chǔ)的算法是因?yàn)楹罄m(xù)的收集算法都是基于這種思路并對(duì)其缺點(diǎn)進(jìn)行改進(jìn)得到的。它的缺點(diǎn)主要有兩個(gè):
一個(gè)是效率問題,標(biāo)記和清除過程效率都不高。
另外一個(gè)是空間問題,標(biāo)記清除后會(huì)產(chǎn)生大量不連線內(nèi)存碎片,內(nèi)存碎片太多導(dǎo)致當(dāng)程序運(yùn)行進(jìn)需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集操作。標(biāo)記-清除算法的執(zhí)行過程如下圖:

2.復(fù)制算法——Coping——新生代回收采用——Eden、Survivor空間分配
為了解決效率問題,“復(fù)制”收集算法出現(xiàn)了,它將可用內(nèi)存按容量分為大小相等的兩塊,每次使用其中一塊。當(dāng)這一塊內(nèi)存使用完了(Eden+Survivor:90%),就將還存活著的對(duì)象復(fù)制到另外一塊上(Survivor:10%),然后再把已使用過的內(nèi)存空間一次性清理空。這樣使得每次都是對(duì)其中一塊進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也不用考慮內(nèi)存碎片的問題,只需要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半,代價(jià)太高了一點(diǎn)(根據(jù)研究新生代中的對(duì)象98%是朝生夕死的,所以內(nèi)存分配一般為8:1:1)。復(fù)制算法執(zhí)行過程如下圖:

現(xiàn)在商業(yè)虛擬機(jī)都是采用這種算法來回收新生代,IBM的專門研究表明,新生代中的對(duì)象98%是朝生夕死的,生命周期很短,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間與兩塊較小的Survivor空間,每次使用Eden與其中一塊Survivor空間。當(dāng)回收時(shí),將Eden與Survivor中還存活的對(duì)象一次性地拷貝到另外一塊Survivor空間中,最后清理掉Eden和剛才使用過的Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor空間比例為8:1,也就是每次新生代中可用內(nèi)存為整個(gè)新生代容量的90%(80%+10%),只有10%的新生代內(nèi)存是“浪費(fèi)”的。當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),但沒有辦法保證每回收都只有不多于10%對(duì)象存活,當(dāng)Survivor空間不足時(shí),需要依賴其它內(nèi)存(老年代)進(jìn)行分配擔(dān)保。
3.標(biāo)記-整理算法——Mark-Compact——老年代(方法區(qū))回收采用
復(fù)制算法在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(fù)制操作,效率將會(huì)變低,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。根據(jù)老年代的特點(diǎn),“標(biāo)記-整理”算法被提出,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清除,而是讓所有存活對(duì)象都向一端移動(dòng),然后直接清除掉端邊界以外的內(nèi)存?!皹?biāo)記-整理”算法執(zhí)行示意圖如下:

4.分代收集算法——Generational Collection
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”(Generational Collection)算法,該算法將根據(jù)對(duì)象存活周期不同將內(nèi)存劃分為幾塊。一般把Java堆分為新生代與老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/strong>在新生代中,每次垃圾收集都發(fā)現(xiàn)有大量對(duì)象死去,只有少量對(duì)象存活,就選得復(fù)制收集算法,只要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高、沒有額外的空間對(duì)其進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清除”或“標(biāo)記-整理”算法進(jìn)行回收。
垃圾收集器
如果說收集算法是內(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è)年代所使用的收集器。下面是Sun HotSpot虛擬機(jī)1.6版本Update22包含的所有收集器:

上圖中,如果兩個(gè)收集器之間存在連線,就說明它們可以搭配使用。
1.Serial收集器——單線程復(fù)制算法
Serial收集器是最基本、歷史最悠久的收集器,曾經(jīng)(在JDK1.3.1之前)是虛擬機(jī)新生代的唯一選擇。這是一個(gè)單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會(huì)使用一個(gè)CPU或一條收集線程去完成垃圾工作,更重要的是它進(jìn)行垃圾回收時(shí),必須(STW)暫停其它所有工作線程(Sun將這件事情稱之為“Stop The World”),直到它收集結(jié)束。下面是Serial/Serial Old收集器的運(yùn)行過程:

到目前為止,Serial收集器是虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)重新代收集器。它簡(jiǎn)單而高效(與其它收集器的單線程相比),對(duì)于限定單個(gè)CPU環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。在桌面應(yīng)用場(chǎng)景中,分配給虛擬機(jī)的內(nèi)存一般來說不會(huì)太大,收集幾十兆甚至一兩百兆的新生代,停頓時(shí)間完全可以控制在幾十毫秒最多一百多毫秒內(nèi),只要不是頻繁發(fā)生,這點(diǎn)停頓是可以接受的。所以,Serial收集器對(duì)于運(yùn)行在Client模式下的虛擬機(jī)來說是一個(gè)很好的選擇。
2.ParNew收集器——多線程復(fù)制算法
ParNew收集器是Serial收集器的多線程版本,除了使用多條線程進(jìn)行垃圾收集之外,其余行為包含Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對(duì)象分配規(guī)則、回收策略等都與Serial收集器完全一樣。ParNew收集器的工作過程如下圖:

3.Parallel Scavenge收集器——多線程復(fù)制算法——控制吞吐量
Parallel Scavenge收集器也是一個(gè)新生代收集器,它也是使用復(fù)制收集算法,又是并行的多線程垃圾收集器。其特點(diǎn)是與其它收集器的關(guān)注點(diǎn)不同,CMS等收集的關(guān)注點(diǎn)是盡可能縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而Parallel Scavenge收集器的目的是達(dá)到一個(gè)可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運(yùn)行用戶代碼的時(shí)候與CPU總消耗時(shí)間的比值,即吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收回時(shí)間)。停頓時(shí)間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶的體驗(yàn);而高吞吐量可以高效率地利用CPU時(shí)間,盡快的完成程序任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。Parallel Scavenge收集器提供了兩個(gè)參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時(shí)間的-XX:MaxGCPauseMillis及直接設(shè)置吞吐量大小的-XX:GCTimeRatio。Parallel Scavenge收集器還有一個(gè)參數(shù)-XX:UseAdaptiveSizePolicy,這是一個(gè)開關(guān)參數(shù),當(dāng)這個(gè)參數(shù)打開之后,就不需要手工指定新生代大小、Eden與Survivor區(qū)的比例,晉升老年代對(duì)象年齡等細(xì)節(jié)參數(shù)了。虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)用使這些參數(shù)以提供最合適的停頓時(shí)間或最大的吞吐量,這種調(diào)節(jié)方式稱為GC自適應(yīng)調(diào)用策略(GC Ergonomics)。
4.Serial Old收集器——單線程標(biāo)記-整理算法
Serial Old是Serial收集器的老年代版本,它同樣是一個(gè)單線程收集器,使用”標(biāo)記-整理“算法。這個(gè)收集器的主要意義也是被client模式下的虛擬機(jī)使用。如果在server模式下,它主要有兩大用途:
一個(gè)是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用;
另外一個(gè)就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure的時(shí)候使用。
Serial Old收集器的工作過程如下圖:

5.Parallel Old收集器——多線程標(biāo)記-整理算法
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和”標(biāo)記-整理“算法。這個(gè)收集器是在JDK1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態(tài)。原因是如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器之外別無選擇(因?yàn)樗鼰o法與CMS配合使用 )。注重吞吐量以及CPU資源敏感的場(chǎng)合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器。 Parallel Old收集器的工作過程如下圖:

6.CMS收集器——標(biāo)記-清除算法——以獲取最短回收停頓時(shí)間為目標(biāo)
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。目前很大一部分的Java應(yīng)用都集中在互聯(lián)網(wǎng)網(wǎng)站或B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,CMS收集器就非常符合這應(yīng)用的需求。CMS收集器是基于”標(biāo)記-清除“算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)前面幾種收集器來說要復(fù)雜一點(diǎn),
整個(gè)過程分為4個(gè)步驟,包括:
初始標(biāo)記(CMS initial mark)——STW——標(biāo)記與GC Roots能直接關(guān)聯(lián)到的對(duì)象
并發(fā)標(biāo)記(CMS concurrent mark)——可達(dá)性分析(根搜索過程)
重新標(biāo)記(CMS remark)——STW——修正并發(fā)標(biāo)記期間,因?yàn)橛脩舫绦蚶^續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄
并發(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 Roots Tracing的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因?yàn)橛脩舫绦蚶^續(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í)行的。執(zhí)行圖如下:

CMS是一款優(yōu)秀的收集器,它的主要優(yōu)點(diǎn)在名字上已經(jīng)體現(xiàn)出來了:并發(fā)收集、低停頓。
但它有三個(gè)顯著缺點(diǎn):
CMS收集器對(duì)CPU資源非常敏感。在并發(fā)階段,它雖然不會(huì)導(dǎo)致用戶線程停頓,但是會(huì)因?yàn)檎加昧艘徊糠志€程(或者說CPU資源)而導(dǎo)致應(yīng)用程序變慢,總吞吐量會(huì)降低。CMS默認(rèn)啟動(dòng)的回收線程數(shù)是(CPU數(shù)量+3)/4,也就是當(dāng)CPU在4個(gè)以上時(shí)((4+3)/4=1),并發(fā)回收時(shí)垃圾收集線程不少于25%的CPU資源,并且隨著CPU數(shù)量的增加而下降。但是當(dāng)CPU不足4個(gè)(譬如2個(gè))時(shí),CMS對(duì)用戶程序的影響就可能變得很大,如果本來CPU負(fù)載就比較大,還分出一半的運(yùn)算能力去執(zhí)行收集器線程,就可能導(dǎo)致用戶程序的執(zhí)行速度忽然降低了50%。
CMS收集器無法處理浮動(dòng)垃圾,可能出現(xiàn)“Concurrent Mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生。由于CMS并發(fā)清理階段用戶線程還在運(yùn)行著,伴隨程序運(yùn)行自然就還會(huì)有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,CMS無法在當(dāng)次收集中處理掉它們,只好留待下一次GC時(shí)再清理掉。這一部分垃圾就稱為“浮動(dòng)垃圾”。因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。
CMS是一款基于“標(biāo)記-清除”算法實(shí)現(xiàn)的收集器,這意味著收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生。空間碎片過多時(shí),將會(huì)給大對(duì)象分配帶來很大麻煩,往往會(huì)出現(xiàn)老年代還有很大空間剩余,但是無法找到足夠大的連續(xù)空間來分配當(dāng)前對(duì)象,不得不提前觸發(fā)一次Full GC。為了解決這個(gè)問題,CMS收集器提供了一個(gè)-XX:+UseCMSCompactAtFullCollection開關(guān)參數(shù)(默認(rèn)就是開啟的),用于在CMS收集器頂不住要進(jìn)行FullGC時(shí)開啟內(nèi)存碎片的合并整理過程(申請(qǐng)Serial Old 收集器進(jìn)行標(biāo)記整理),內(nèi)存整理的過程是無法并發(fā)的,空間碎片問題沒有了,但停頓時(shí)間不得不變長(zhǎng)。虛擬機(jī)設(shè)計(jì)者還提供了另外一個(gè)參數(shù)-XX:CMSFullGCsBeforeCompaction,這個(gè)參數(shù)是用于設(shè)置執(zhí)行多次不壓縮的Full GC后,跟著來一次帶壓縮的(默認(rèn)值為0,表示每次進(jìn)入Full GC時(shí)都進(jìn)行碎片整理:Serial Old 收集器)。
7.G1收集器
Java Hotspot G1 GC的一些關(guān)鍵技術(shù)
G1收集器是垃圾收集器理論進(jìn)一步發(fā)展的產(chǎn)物,它與前面的CMS收集器相比有兩個(gè)顯著改進(jìn):一是G1收集器是基于“標(biāo)記-整理”算法實(shí)現(xiàn),也就是說它不會(huì)產(chǎn)生內(nèi)存碎片,這對(duì)于長(zhǎng)時(shí)間運(yùn)行的應(yīng)用系統(tǒng)來說非常重要。二是它可以非常精確地控制停頓,即能讓使用都明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不超過N毫秒,這幾乎已經(jīng)是實(shí)時(shí)Java(RTSJ)的垃圾收集器的特征了。G1收集器可以實(shí)現(xiàn)在基本不犧牲吞量的前提下完成低停頓的內(nèi)存回收,這是由于它能夠極力地避免全區(qū)域的垃圾收集,之前的收集器進(jìn)行收集的范圍都是整個(gè)新生代或老年代,而G1將Java堆(包含新生代與老年代)劃分為多個(gè)大小固定的獨(dú)立區(qū)域,并且跟蹤這些區(qū)域里的垃圾堆積程度,在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收垃圾最多的區(qū)域(這就是Garbage First名稱的由來)。區(qū)域劃分及優(yōu)先級(jí)的區(qū)域回收,保證了G1收集器在有限時(shí)間內(nèi)可以獲得最高的收集效率。
G1是一款面向服務(wù)端應(yīng)用的垃圾收集器。HotSpot開發(fā)團(tuán)隊(duì)賦予它的使命是(在比較長(zhǎng)期的)未來可以替換掉JDK 1.5中發(fā)布的CMS收集器。與其他GC收集器相比,G1具備如下特點(diǎn):
并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè)CPU來縮短Stop-The-World停頓的時(shí)間,部分其他收集器原本需要停頓Java線程執(zhí)行的GC動(dòng)作,G1收集器仍然可以通過并發(fā)的方式讓Java程序繼續(xù)執(zhí)行。
分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨(dú)立管理整個(gè)GC堆,但它能夠采用不同的方式取處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(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)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC。
可預(yù)測(cè)的停頓:這是G1相對(duì)于CMS的另一大優(yōu)勢(shì),降低停頓時(shí)間是G1和CMS共同的關(guān)注點(diǎn),但G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過N毫秒,這幾乎已經(jīng)是實(shí)時(shí)Java(RTSJ:Real Time specification for Java)的垃圾收集器的特征了。
在G1之前的其他收集器進(jìn)行收集的范圍都是整個(gè)新生代或者老年代,而G1不再是這樣。使用G1收集器時(shí),Java堆的內(nèi)存布局就與其他收集器很很大差別,它將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的,它們都是一部分Region(不需要連續(xù))的集合。
G1收集器之所以能建立可預(yù)測(cè)的停頓時(shí)間模型,是因?yàn)樗梢?strong>有計(jì)劃地避免在整個(gè)Java堆中進(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名稱的來由)。這種使用Region劃分內(nèi)存空間以及有優(yōu)先級(jí)的區(qū)域回收方式,保證了G1收集器在有限的時(shí)間內(nèi)可以獲取盡可能高的收集效率。
G1收集器的運(yùn)作大致可劃分為以下幾個(gè)步驟:類似與CMS收集過程。
初始標(biāo)記(Initial Marking)
并發(fā)標(biāo)記(Concurrent Marking)
最終標(biāo)記(Final Marking)
篩選回收(Live Data Counting and Evacuation)
G1收集器的運(yùn)作步驟中并發(fā)和需要停頓的階段:

8.zgc
9.shenandoah(謝南多厄)
疑問:
Q: 為什么CMS垃圾收集器不能與Parallel Scavenge收集器組合使用呢?
Q: GC優(yōu)化?
從實(shí)際案例聊聊Java應(yīng)用的GC優(yōu)化
Q: G1將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的,它們都是一部分Region(不需要連續(xù))的集合。是什么樣的模型?怎么理解?
圖解G1?
Q: 互聯(lián)網(wǎng)網(wǎng)站或B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,因此一般的網(wǎng)絡(luò)服務(wù)項(xiàng)目都是更注重相應(yīng)速度甚于吞吐量的是么?也因此更多的垃圾收集器會(huì)選擇ParNew + CMS + Serial Old 的組合?
Q: 重新標(biāo)記(CMS remark)是為了修正并發(fā)標(biāo)記期間,因?yàn)橛脩舫绦蚶^續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這時(shí)候是將并發(fā)標(biāo)記期間有些GC Roots 已經(jīng)不再存在了,如虛擬機(jī)棧中的局部變量表引用的對(duì)象?這部分GC Roots所引用的對(duì)象便可以回收了是么?重新標(biāo)記還是去標(biāo)記與GC Roots能直接關(guān)聯(lián)到的對(duì)象是么?
Q: 各種垃圾收集器的控制參數(shù)設(shè)置?
如Serial、ParNew 的控制參數(shù)設(shè)置:
-XX:SurvivorRatio、
-XX:PretenureSizeThreshold、
-XX:HandlePromotionFailure
Parallel Scavenge的控制參數(shù)設(shè)置:
分別是控制最大垃圾收集停頓時(shí)間的-XX:MaxGCPauseMillis及直接設(shè)置吞吐量大小的-XX:GCTimeRatio。Parallel Scavenge收集器還有一個(gè)參數(shù)-XX:UseAdaptiveSizePolicy,這是一個(gè)開關(guān)參數(shù),當(dāng)這個(gè)參數(shù)打開之后,就不需要手工指定新生代大小、Eden與Survivor區(qū)的比例,晉升老年代對(duì)象年齡等細(xì)節(jié)參數(shù)了。虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)用使這些參數(shù)以提供最合適的停頓時(shí)間或最大的吞吐量,這種調(diào)節(jié)方式稱為GC自適應(yīng)調(diào)用策略(GC Ergonomics)。
Q: 吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收回時(shí)間)。停頓時(shí)間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶的體驗(yàn);而高吞吐量可以高效率地利用CPU時(shí)間,盡快的完成程序任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。為什么說Parallel Scavenge收集器適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)?
A: Parallel Scavenge收集器的目的是實(shí)現(xiàn)高吞吐,而高吞吐必然會(huì)犧牲一點(diǎn)延遲,Parallel Scavenge收集器適合服務(wù)器端的新生代收集器。
重要參考:為什么 JDK 8 默認(rèn)使用 Parallel Scavenge 收集器?
Q:延伸問題: Java 作為服務(wù)端時(shí)更關(guān)注是低延遲還是高吞吐?為什么CMS 就從來沒有作為默認(rèn)垃圾收集器使用過? 或者說為什么 JDK 8默認(rèn)垃圾收集器沒有選擇 ParNew + CMS + Serial Old 的組合 。
A:Java 作為服務(wù)端時(shí)更關(guān)注是高吞吐。而CMS沒有作為默認(rèn)垃圾收集器使用的原因是:
CMS的缺點(diǎn)有:
CMS在GC時(shí)會(huì)對(duì)CPU有比較大的壓力,形成典型的CPU Spike(CPU毛刺)。
CMS僅針對(duì)老年代,還需要一個(gè)年輕代的收集器。CMS又和Parallel Scavenge不兼容,只能和ParNew湊合,然而ParNew又不如Parallel Scavenge先進(jìn)。
CMS沒法處理浮動(dòng)垃圾,并發(fā)標(biāo)記過程中死亡的對(duì)象只能留到以后的GC處理。
Mark-Sweep算法對(duì)內(nèi)存碎片無能為力,內(nèi)存碎片太多,觸發(fā)了Concurrent Mode Failure還不是得去請(qǐng)Serial Old來收拾爛攤子,結(jié)果就是STW。
結(jié)合目前的發(fā)展:
G1這種革命性的GC日趨成熟,可以管理整個(gè)堆區(qū),比CMS強(qiáng)太多,更不用說ZGC和Shenandoah。
CMS的實(shí)現(xiàn)復(fù)雜(CMS的參數(shù)有70多個(gè),而G1只有26個(gè)),維護(hù)的難度可想而知。
cms并不是一個(gè)非常成功的gc策略,要協(xié)調(diào)CMS時(shí)需要調(diào)整的參數(shù)太多,相比之下g1要好太多 ,G1沒那么多參數(shù)要協(xié)調(diào)。雖然cms對(duì)比比g1可以達(dá)到低延遲的效果,參數(shù)協(xié)調(diào)好了的話,可以做到major gc一次都不出現(xiàn),只觸發(fā)minor gc,然后minor可以壓縮到10ms以內(nèi)。但CMS協(xié)調(diào)好的這種效果被zgc所實(shí)現(xiàn),而zgc又不需要你協(xié)調(diào)任何參數(shù),jvm會(huì)幫你把這一切搞定,zgc承諾10ms以內(nèi)完成gc,而且實(shí)測(cè),幾個(gè)t的內(nèi)存,gc停頓普遍在1ms左右,少數(shù)達(dá)到2ms,長(zhǎng)期目標(biāo)是所有zgc都在1ms以內(nèi)完成,所以你可以認(rèn)為zgc是cms的完美替代品,更簡(jiǎn)單,性能更好,所以cms被淘汰。
JDK 15開始,zgc和shenandoah也將成為正式的gc策略,使用這兩個(gè)垃圾收集器不需要協(xié)調(diào)什么,只需要知道怎么開這兩個(gè)gc策略就行了。
zgc適合客戶端編程,尤其是對(duì)latency敏感的場(chǎng)合使用,比如我們寫javafx時(shí)候,就會(huì)開zgc。
shenandoah(謝南多厄 )適合服務(wù)端等更在意throughput的場(chǎng)合使用,比如用es4x的時(shí)候,就用shenandoah。
Q: 延伸問題:concurrent mode failure ?
A:concurrent mode failure是CMS垃圾收集器特有的錯(cuò)誤,CMS的垃圾清理和用戶線程是并行進(jìn)行的,如果在并行清理的過程中老年代的空間不足以容納應(yīng)用產(chǎn)生的垃圾(也就是老年代正在清理,從年輕代晉升了新的對(duì)象,或者直接分配大對(duì)象年輕代放不下導(dǎo)致直接在老年代生成,這時(shí)候老年代也放不下),則會(huì)拋出“concurrent mode failure”。
concurrent mode failure影響:會(huì)使老年代的垃圾收集器從CMS退化為Serial Old,所有應(yīng)用線程被暫停,停頓時(shí)間變長(zhǎng)。
可能原因及方案:
原因1:CMS觸發(fā)太晚
方案:將-XX:CMSInitiatingOccupancyFraction=N調(diào)?。?strong>調(diào)小老年代的空間 )
原因2:空間碎片太多
方案:開啟空間碎片整理,并將空間碎片整理周期設(shè)置在合理范圍;
-XX:+UseCMSCompactAtFullCollection (空間碎片整理)
-XX:CMSFullGCsBeforeCompaction=n
原因3:垃圾產(chǎn)生速度超過清理速度
晉升閾值過小;
Survivor空間過??;
Eden區(qū)過小,導(dǎo)致晉升速率提高;
存在大對(duì)象;
Q: jdk7、8、9默認(rèn)垃圾回收器?
jdk1.7 默認(rèn)垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默認(rèn)垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默認(rèn)垃圾收集器G1
解釋:UseParallelGC 即 Parallel Scavenge + Parallel Old 。
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n475" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
-XX:+PrintCommandLineFlagsjvm參數(shù)可查看默認(rèn)設(shè)置收集器類型
?
-XX:+PrintGCDetails亦可通過打印的GC日志的新生代、老年代名稱判斷</pre>
內(nèi)存分配與回收策略
Java技術(shù)體系中的自動(dòng)內(nèi)存管理最終可以歸結(jié)為自動(dòng)化地解決了兩個(gè)問題:給對(duì)象分配內(nèi)存以及回收分配給對(duì)象的內(nèi)存。對(duì)象的內(nèi)存分配往大的方向上講,就是在堆上分配,對(duì)象主要分配在新生代的Eden區(qū)上,如果啟動(dòng)了本地線程分配緩沖(-XX:+UseTLAB,默認(rèn)已開啟),將按線程優(yōu)先在TLAB上分配。少數(shù)情況下也可能會(huì)直接分配在老年代中(如大對(duì)象像比較大的數(shù)組或字符串這類),分配的規(guī)則并不是百分之百固定的,其細(xì)節(jié)取決于當(dāng)前使用的是哪一種垃圾收集器組合,還有虛擬機(jī)中與內(nèi)存相關(guān)的參數(shù)設(shè)置。下面是幾條主要的最普遍的內(nèi)存分配規(guī)則:
1.對(duì)象優(yōu)先在Eden分配
大多數(shù)情況下,對(duì)象在新生代的Eden區(qū)中分配。當(dāng)Eden區(qū)沒有足夠的空間進(jìn)行分配時(shí),虛擬將發(fā)起一次Minor GC,如果GC后新生代中存活的對(duì)象無法全部放入Survivor空間,則需要通過分配擔(dān)保機(jī)制提前進(jìn)入到老年代中,前提是老年代中不能容納所有存活對(duì)象,即只能容納部分。則未能進(jìn)入到老年代的存活對(duì)象將繼續(xù)分配在Eden區(qū)中,如果Eden區(qū)也還未能容納剩余的存活對(duì)象虛擬機(jī)拋出OutOfMemoryError錯(cuò)誤。虛擬機(jī)提供了-XX:+PrintGCDetails參數(shù)用于輸出收集器日志參數(shù)。
Minor GC與Full GC的區(qū)別:a.新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)镴ava對(duì)象大多都具備朝生夕死的特性,所以Minor GC非常頻繁,一般回收速度也比較快。b.老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC(正常情況下是全堆的收集,會(huì)伴隨一次Minor GC),出現(xiàn)了Major GC,經(jīng)常會(huì)伴隨至少一次Minor GC(但非絕對(duì),在ParallelScavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過程)。MajorGC的速度一般會(huì)比MinorGC慢10倍以上。
2.大對(duì)象直接進(jìn)入老年代
所謂大對(duì)象是指,需要大量連續(xù)內(nèi)存空間的Java對(duì)象,最典型的大對(duì)象就是那種很長(zhǎng)的字符串及數(shù)組。大對(duì)象對(duì)虛擬機(jī)的內(nèi)存分配來說是一個(gè)壞消息,經(jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來“安置”它們。虛擬機(jī)提供了一個(gè) -XX:PretenureSizeThreshold參數(shù),令大于這個(gè)設(shè)置值的對(duì)象直接在老年代中分配。這樣做的目的是避免在Eden區(qū)及兩個(gè)Survivor區(qū)之間發(fā)生大量的內(nèi)存拷貝。
3.長(zhǎng)期存活對(duì)象將進(jìn)入老年代
虛擬機(jī)采用了分代收集的思想來管理內(nèi)存,那么內(nèi)存回收時(shí)就必須能夠識(shí)別哪些對(duì)象應(yīng)當(dāng)放在新生代,哪些對(duì)象應(yīng)該放在老年代。為了做到這點(diǎn),虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡計(jì)數(shù)器。如果對(duì)象在Eden區(qū)出生并經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor區(qū)容納的話,將被移到Survivor區(qū)中,并將對(duì)象年齡設(shè)置為1。對(duì)象在Survivor區(qū)中每熬過一次Minor GC,年齡就增加1歲。當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲),就會(huì)被晉升到老年代中。對(duì)象晉升老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold來設(shè)置。
4.動(dòng)態(tài)對(duì)象年齡判定
為了更好的適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不總是要求對(duì)象年齡必須達(dá)到MaxTenuringThreshold才能晉升到老年氏,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,那么年齡大于或等于該年齡的對(duì)象就直接進(jìn)行老年代,無須等到MaxTenuringThreshold中要求的年齡。
5.空間分配擔(dān)保
在發(fā)生Minor GC時(shí),虛擬機(jī)會(huì)檢測(cè)之前每次晉升到老年代的平均大小是否大于老年代剩余空間的大小,如果大于,則改為直拉進(jìn)行一次Full GC。如果小于,則查看HandlePromotionFailure設(shè)置是否允許擔(dān)保失敗;如果允許,那只會(huì)進(jìn)行Minor GC;如果不允許,則要改為進(jìn)行一次Full GC。
新生代使用復(fù)制收集算法,但為了提高內(nèi)存利用率,只使用其中一個(gè)Survivor空間來作為輪換備份,因此當(dāng)出現(xiàn)大量對(duì)象在Minor GC后仍然存活的情況時(shí),就需要老年代進(jìn)行分配擔(dān)保,讓Survivor空間無法容納的對(duì)象直接進(jìn)入老年代。
取平均值進(jìn)行比較仍然是一種動(dòng)態(tài)概率的手段,也就是說如果某次Minor GC存活的對(duì)象突增,遠(yuǎn)高于平均值的話,依然會(huì)導(dǎo)致擔(dān)保失敗(HandlePromotionFailure)。如果出現(xiàn)了HandlePromotionFailure,那只好在失敗后重新發(fā)起一次Full GC。雖然擔(dān)保失敗時(shí)繞圈子是最大的,但是大部情況下還是會(huì)將HandlePromotionFailure開關(guān)打開,避免Full GC過于頻繁。
疑問:
Q: JAVA的TLAB是什么?
Q: 對(duì)象主要分配在新生代的Eden區(qū)上,如果啟動(dòng)了本地線程分配緩沖(-XX:+UseTLAB,默認(rèn)已開啟),將按線程優(yōu)先在TLAB上分配。這句話怎么理解?
Q: 如果GC后新生代中存活的對(duì)象無法全部放入Survivor空間,則需要通過分配擔(dān)保機(jī)制提前進(jìn)入到老年代中,前提是老年代中不能容納所有存活對(duì)象,即只能容納部分。則未能進(jìn)入到老年代的存活對(duì)象將繼續(xù)分配在Eden區(qū)中,如果Eden區(qū)也還未能容納剩余的存活對(duì)象虛擬機(jī)拋出OutOfMemoryError錯(cuò)誤。
在空間分配擔(dān)保這節(jié)中,是這樣描述的:新生代使用復(fù)制收集算法,但為了提高內(nèi)存利用率,只使用其中一個(gè)Survivor空間來作為輪換備份,因此當(dāng)出現(xiàn)大量對(duì)象在Minor GC后仍然存活的情況時(shí),就需要老年代進(jìn)行分配擔(dān)保,讓Survivor空間無法容納的對(duì)象直接進(jìn)入老年代。
那么問題來了:當(dāng)GC后新生代中存活的對(duì)象無法全部放入Survivor空間時(shí),分配擔(dān)保機(jī)制對(duì)這些存活的對(duì)象在新生代和老年代是如何分配內(nèi)存的,為什么不是全部進(jìn)入老年代?到底是哪種方式呢?是先優(yōu)先填滿Survivor空間,剩余的進(jìn)入老年代?
Q: 當(dāng)空間分配擔(dān)保沒有打開的時(shí)候,JVM進(jìn)行Minor GC時(shí)是如何判斷什么時(shí)候進(jìn)行Minor GC什么時(shí)候進(jìn)行Major GC 的呢?在每次晉升到老年代的平均大小小于老年代剩余空間的大小時(shí),老年代空間大小剩余空間占比多少的時(shí)候呢?
class類文件結(jié)構(gòu)概述
class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格緊湊地排列在class文件中,中間沒有任何分隔符。當(dāng)遇到需要占用8位字節(jié)以上的的數(shù)據(jù)項(xiàng)時(shí),則會(huì)按照高位在前的方式分隔成若干個(gè)8位字節(jié)進(jìn)行存儲(chǔ)。根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,class文件格式采用一種類似于C語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存儲(chǔ),這種偽結(jié)構(gòu)只有兩種數(shù)據(jù)類型:無符號(hào)數(shù)和表。無符號(hào)數(shù)屬于基本數(shù)據(jù)類型,以u(píng)1、u2、u4、u8來分別代碼1個(gè)字節(jié)、2個(gè)字節(jié)、4個(gè)字節(jié)、8個(gè)字節(jié)的無符號(hào)數(shù),無符號(hào)數(shù)可以用于描述數(shù)字、索引引用、數(shù)量值,或者按照UTF-8編碼構(gòu)成的字符串值。表是由多個(gè)符號(hào)數(shù)或其它表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類型,所有表都習(xí)慣性地以“_info”結(jié)尾。表用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu),整個(gè)class文件本質(zhì)上就是一張表,它由如下數(shù)據(jù)項(xiàng)構(gòu)成:

無論是無符號(hào)數(shù)還是表,當(dāng)需要描述同一類型但數(shù)量不定的多個(gè)數(shù)據(jù)時(shí),經(jīng)常會(huì)使用一個(gè)前置的容量計(jì)數(shù)器加若干個(gè)連續(xù)的數(shù)據(jù)項(xiàng)的形式,這時(shí)候稱這一系列連續(xù)的某一類型的數(shù)據(jù)為某一類型的集合。
Q: 無符號(hào)數(shù)可以用于描述數(shù)字、索引引用、數(shù)量值,或者按照UTF-8編碼構(gòu)成的字符串值。索引引用指的是什么?對(duì)象的引用么?
class類文件魔數(shù),版本,常量池
魔數(shù)——magic——是否為一個(gè)能被虛擬機(jī)接受的class文件
每個(gè)class文件的頭4個(gè)字節(jié)稱為魔數(shù)(Magic Number),其值為:0xCAFEBABE,它的唯一作用是用于確定這個(gè)文件是否為一個(gè)能被虛擬機(jī)接受的class文件。使用魔數(shù)而不是擴(kuò)展名來進(jìn)行識(shí)別主要是基于安全的考慮,因?yàn)槲募臄U(kuò)展名可以隨意地被改動(dòng)。
版本號(hào)——minor_version、major_version
緊接著魔的4個(gè)字節(jié)存儲(chǔ)的是class文件的版本號(hào):第5和第6個(gè)字節(jié)是次版本號(hào)(Minor Version),第7和第8個(gè)字節(jié)是主版本號(hào)(Major Version)。java的版本是從45開始的,JDK1.1之后的每個(gè)JDK大版本發(fā)布主版本號(hào)上加1(JDK1.0-1.1使用了45.0-45.3的版本號(hào)),高版本的JDK能向下兼容以前版本的class文件,但不能運(yùn)行以后版本的class文件,即使文件格式并未發(fā)生變化。JDK1.2對(duì)應(yīng)主版本號(hào)為46,JDK1.3為47,依此類推。
常量池——constant_pool
緊接著主次版本號(hào)之后的是常量池入口,常量池是class文件結(jié)構(gòu)中與其它項(xiàng)目關(guān)聯(lián)最多的數(shù)據(jù)類型,也是占用class文件空間最大的數(shù)據(jù)項(xiàng)目之一,同時(shí)它還是class文件中第一個(gè)出現(xiàn)的表類型數(shù)據(jù)項(xiàng)目。****由于常量池中常量的數(shù)據(jù)是不固定的,所以在常量池的入口需要放置一個(gè)u2類型的數(shù)據(jù),代表常量池容量計(jì)數(shù)值(constant_pool_count)。與Java語言習(xí)慣不一樣的是,這個(gè)容量計(jì)數(shù)是從1而不是0開始的。將第0項(xiàng)常量空出來的目的是為了滿足后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達(dá)“不引用任何一個(gè)常量池項(xiàng)目”的意思。class文件結(jié)構(gòu)中只有常量池的容量計(jì)數(shù)是從1開始,對(duì)于其它集合類型,包括接口索引集合,字段表集合,方法表集合的容量計(jì)算都是從0開始的。常量池中主要存放兩大類常量:字面量(Literal)和符號(hào)引用(Symbolic References)。字面量比較接近于Java語言層面的常量概念,如文本字符串,被聲明為final的常量值等。而符號(hào)引用則屬性編譯原理方面的概念,包含了下面三類常量:
類和接口的全限定名(Fully Qualified Name)
字段的名稱和描述符(Descriptor)
方法的名稱和描述符
常量池中的每一項(xiàng)常量都是一個(gè)表,共有11種結(jié)構(gòu)各不相同的表結(jié)構(gòu)數(shù)據(jù),這11種表都有一個(gè)共同的特點(diǎn),就是表開始的第一位是一個(gè)u1類型的標(biāo)志位,代表當(dāng)前這個(gè)常量屬性哪種常量類型,11種常量類型具體含義如下:


疑問:
Q: 常量池是class文件結(jié)構(gòu)中與其它項(xiàng)目關(guān)聯(lián)最多的數(shù)據(jù)類型,是與其他類還是項(xiàng)目管理,這句話什么意思?叫項(xiàng)目這種說法不正確吧?
Q: 這個(gè)容量計(jì)數(shù)是從1而不是0開始的。將第0項(xiàng)常量空出來的目的是為了滿足后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達(dá)“不引用任何一個(gè)常量池項(xiàng)目”的意思。什么意思?
Q: 常量池中主要存放兩大類常量:字面量(Literal)和符號(hào)引用(Symbolic References)。
訪問標(biāo)志——access_flags
常量池結(jié)束之后,緊接著的2個(gè)字節(jié)代表訪問標(biāo)志(access_flags),這個(gè)標(biāo)志用于識(shí)別一些類或接口層次的訪問信息,包括:這個(gè)class是類還是接口;是否定義為public類型;是否定義為abstract類型;如果是類的話,是否被聲明為final,等等。具體的標(biāo)志以及標(biāo)志的含義如下表:

access_flags中一共有32個(gè)標(biāo)志位可以使用,當(dāng)前只定義了其中的8個(gè),沒有使用到的標(biāo)志位要求一律為0。
