java JVM內(nèi)部結(jié)構(gòu)


java對象創(chuàng)建過程
/**
*
* 關(guān)于Java對象創(chuàng)建的過程:
* new關(guān)鍵字創(chuàng)建對象的3個(gè)步驟:
* 1.在堆內(nèi)存中創(chuàng)建出對象的實(shí)例。
* 2.為對象的實(shí)例成員變量賦初值。
* 3.將對象的引用返回
* 指針碰撞(前提是堆中的空間通過一個(gè)指針進(jìn)行分割,一側(cè)是已經(jīng)被占用的空間,另一側(cè)是未被占用的空間)
* 空閑列表(前提是堆內(nèi)存空間中已被使用與未被使用的空間是交織在一起的,這時(shí),虛擬機(jī)就需要通過一個(gè)列表來記錄哪些空間是可以使用的,
* 哪些空間是已被使用的,接下來找出可以容納下新創(chuàng)建對象的且未被使用的空間,在此空間存放該對象,同時(shí)還要修改列表上的記錄)
* 對象在內(nèi)存中的布局:
* 1.對象頭.
* 2.實(shí)例數(shù)據(jù)(即我們在一個(gè)類中所聲明的各項(xiàng)信息)
* 3.對齊填充(可選) !
* 引用訪問對象的方式:
* 1.使用句柄的方式。
* 2.使用直接指針的方式。
*/
public class MemoryTest1 {
public static void main(String[] args) {
//-Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError 設(shè)置jvm對空間最小和最大以及遇到錯(cuò)誤時(shí)把堆存儲文件打印出來
//打開jvisualvm裝在磁盤上的轉(zhuǎn)存文件
List<MemoryTest1> list = new ArrayList<>();
while (true) {
list.add(new MemoryTest1());
System.gc();
}
}
}
虛擬機(jī)棧溢出測試
/**
*
* 虛擬機(jī)棧溢出測試
*/
public class MemoryTest2 {
private int length;
public int getLength() {
return length;
}
public void test() throws InterruptedException {
length++;
Thread.sleep(1);
test();
}
public static void main(String[] args) {
//測試調(diào)整虛擬機(jī)棧內(nèi)存大小為: -Xss160k,此處除了可以使用JVisuale監(jiān)控程序運(yùn)行狀況外還可以使用jconsole
MemoryTest2 memoryTest2 = new MemoryTest2();
try {
memoryTest2.test();
} catch (Throwable e) {
System.out.println(memoryTest2.getLength());//打印最終的最大棧深度為:2587
e.printStackTrace();
}
}
}
元空間溢出測試
/**
*
* 元空間內(nèi)存溢出測試
* 設(shè)置元空間大小:-XX:MaxMetaspaceSize=100m
* 關(guān)于元空間參考:https://www.infoq.cn/article/java-permgen-Removed
*/
public class MemoryTest3 {
public static void main(String[] args) {
//使用動態(tài)代理動態(tài)生成類
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MemoryTest3.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, ags, proxy) -> proxy.invokeSuper(obj, ags));
System.out.println("Hello World");
enhancer.create();// java.lang.OutOfMemoryError: Metaspace
}
}
JVM命令使用
/**
*
* jmam命令的使用 -clstats<pid>進(jìn)程id to print class loader statistics
* jmap -clstats 3740
*
* jstat -gc 3740
* S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
* 512.0 512.0 0.0 0.0 24064.0 9626.0 86016.0 1004.1 4864.0 3758.2 512.0 409.1 144 0.064 0 0.000 0.064
* MC元空間總大小,MU元空間已使用的大小
*/
public class MemoryTest4 {
public static void main(String[] args) {
while (true)
System.out.println("hello world");
}
//查看java進(jìn)程id jps -l
// 使用jcmd查看當(dāng)前進(jìn)程的可用參數(shù):jcmd 10368 help
//查看jvm的啟動參數(shù) jcmd 10368 VM.flags
// 10368:-XX:CICompilerCount=3 -XX:InitialHeapSize=132120576 -XX:MaxHeapSize=2111832064 -XX:MaxNewSize=703594496
// -XX:MinHeapDeltaBytes=524288 -XX:NewSize=44040192 -XX:OldSize=88080384 -XX:+UseCompressedClassPointers
// -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
}
JVM常用命令
jcmd (從JDK 1. 7開始增加的命令)
1. jcmd pid VM.flags: 查看JVM的啟動參數(shù)
2. jcmd pid help: 列出當(dāng)前運(yùn)行的Java進(jìn)程可以執(zhí)行的操作
3. jcmd pid helpJFR.dump:查看具體命令的選項(xiàng)
4. jcmd pid PerfCounter.print:看JVm性能相關(guān)的參數(shù)
5. jcmd pid VM.uptime:查有JVM的啟動時(shí)長
6. jcmd pid GC.class_ histogram: 查看系統(tǒng)中類的統(tǒng)計(jì)信息
7. jcmd pid Thread.print: 查看線程堆棧信息
8. jcmd pid GC.heap dump filename 導(dǎo)出Heap dump文件, 導(dǎo)出的文件可以通過jvisualvm查看
9. jcmd pid VM.system_ properties:查看JVM的屬性信息
JVM內(nèi)存舉例說明
public void method() {
Object object = new Object();
/*生成了2部分的內(nèi)存區(qū)域,1)object這個(gè)引用變量,因?yàn)? 是方法內(nèi)的變量,放到JVM Stack里面,2)真正Object
class的實(shí)例對象,放到Heap里面
上述 的new語句一共消耗12個(gè)bytes, JVM規(guī)定引用占4
個(gè)bytes (在JVM Stack), 而空對象是8個(gè)bytes(在Heap)
方法結(jié)束后,對應(yīng)Stack中的變量馬上回收,但是Heap
中的對象要等到GC來回收、*/
}
JVM垃圾識別(根搜索算法( GC RootsTracing ))
在實(shí)際的生產(chǎn)語言中(Java、 C#等),都是使用根搜索算法判定對象是否存活。
算法基本思路就是通過一系列的稱為“GCRoots"的點(diǎn)作為起始進(jìn)行向下搜索,當(dāng)一個(gè)對象到GCRoots沒有任何引用鏈( Reference Chain)相連,則證明此對象
是不可用的在Java語言中,GC Roots包括
●在VM棧(幀中的本地變量)中的引用
●方法區(qū)中的靜態(tài)引用
●JNI (即一般說的Native方法) 中的引用
方法區(qū)
Java虛擬機(jī)規(guī)范表示可以不要求虛擬機(jī)在這區(qū)實(shí)現(xiàn)GC,這區(qū)GC的“性價(jià)比”一般比較低
在堆中,尤其是在新生代,常規(guī)應(yīng)用進(jìn)行I次GC一般可以回收70%~95%的空間,而方法區(qū)的GC效率遠(yuǎn)小于此當(dāng)前的商業(yè)JVM都有實(shí)現(xiàn)方法區(qū)的GC,主要回收兩部分內(nèi)容:廢棄常量與無用類
主要回收兩部分內(nèi)容:廢棄常量與無用類
-
類回收需要滿足如下3個(gè)條件:
- 該類所有的實(shí)例都已經(jīng)被GC,也就是JVM中不存在該Class的任何實(shí)例
- 加載該類的ClassL oader已經(jīng)被GC
- 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法
在大量使用反射、動態(tài)代理、CGLib等字節(jié)碼框架、動態(tài)生成JSP以及OSGi這類頻繁自定義Classloader的場景都需要JVM具備類卸載的支持以保證方法區(qū)不會溢出
垃圾判斷與GC算法
-
垃圾判斷的算法
- 引用計(jì)數(shù)算法(Reference Counting)
- 根搜索算法( GC RootsTracing )
- 在實(shí)際的生產(chǎn)語言中(Java、 C#等)都是使用根搜索算法判定對象是否存活
- 算法基本思路就是通過一一系列的稱為GCRoots"的點(diǎn)作為起始進(jìn)行向下搜索,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈(Reference Chain)相連,則證明此對象是不可用的
-
在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象。
- 方法區(qū)中類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用的對象。
- 本地方法棧中JNI(即一般說的Native方法)引用的對象

- 標(biāo)記-清除算法(Mark Sweep)
- 標(biāo)記-整理算法(Mark-Compact)
- 復(fù)制算法(Copying)
- 分代算法(Generational)
標(biāo)記一清除算法(Mark-Sweep)
算法分為“標(biāo)記”和“清除”兩個(gè)階段,
首先標(biāo)記出所有需要回收的對象,然后回
收所有需要回收的對象缺點(diǎn)
效率問題,標(biāo)記和清理兩個(gè)過程效率都不高
空間問題,
標(biāo)記清理之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致后續(xù)使用中無法找到足夠的連續(xù)內(nèi)存而提前觸發(fā)另一次的垃圾搜集動作效率不高,需要掃描所有對象。堆越大,GC越慢
存在內(nèi)存碎片問題。GC次數(shù)越多,碎片越為嚴(yán)重

復(fù)制(Copying) 搜集算法
將可用內(nèi)存劃分為兩塊,每次只使用其中的一塊,當(dāng)一半?yún)^(qū)內(nèi)存用完了,僅將還存活
的對象復(fù)制到另外一塊上面,然后就把原來整塊內(nèi)存空間一次性清理掉,這樣使得每次內(nèi)存回收都是對整個(gè)半?yún)^(qū)的回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針,按順序分配內(nèi)存就可以了,實(shí)現(xiàn)簡單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半,代價(jià)高昂
現(xiàn)在的商業(yè)虛擬機(jī)中都是用了這一種收集算法來回收新生代
將內(nèi)存分為一塊較大的eden空間和2塊較少的survivor空間,每次使用eden和其中一塊
survivor, 當(dāng)回收時(shí)將eden和survivor還存活的對象一次性拷 貝到另外一塊survivor空間上,然后清理掉eden和用過的survivorOracle Hotspot虛擬機(jī)默認(rèn)eden和survivor的大小比例是8:1,也就是每次只有10%的內(nèi)存是“浪費(fèi)”的
復(fù)制收集算法在對象存活率高的時(shí)候,效率有所下降
如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保用于應(yīng)付半?yún)^(qū)內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法

- 只需要掃描存活的對象,效率更高
- 不會產(chǎn)生碎片
- 需要浪費(fèi)額外的內(nèi)存作為復(fù)制區(qū)
- 復(fù)制算法非常適合生命周期比較短的對象,因?yàn)槊看蜧C總能回收大部分的對象,復(fù)制的開銷比較小
- 根據(jù)IBM的專i研究,98%的Java對象只會存活1個(gè)GC周期,對這些對象很適合用復(fù)制算法。而且
不用1: 1的劃分工作區(qū)和復(fù)制區(qū)的空間
標(biāo)記一整理( Mark-Compact )算法
標(biāo)記過程仍然樣,但后續(xù)步驟不是進(jìn)行直接清理,而是令所有存活的對象一端移動,然后直接清理掉這端邊界以外的內(nèi)存。
沒有內(nèi)存碎片
比Mark-Sweep耗費(fèi)更多的時(shí)間進(jìn)行compact
分代收集。( GenerationalCollecting)算法
- 當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都是采用“分代收集”( Generational Collecting)算法,根據(jù)對象不同的存活周期將內(nèi)存劃分為幾塊。
- 一般是把Java堆分作新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴?,譬如新生代每次GC都有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本,就可以完成收集。
Hotspot JVM 6中共劃分為三個(gè)代:
- 年輕代(Young Generation)
- 老年代(Old Generation)和
- 永久代( Permanent Generation)

年輕代(Young Generation)
新生成的對象都放在新生代。年輕代用復(fù)制算法進(jìn)行GC (理論上年輕代對象的生命周期非常短,所以適合復(fù)制算法)年輕代分三個(gè)區(qū)。一個(gè)Eden區(qū),兩個(gè)Survivor區(qū)(可以通過參數(shù)設(shè)置Survivor個(gè)數(shù))。對象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時(shí),還存活的對象將被復(fù)制到一個(gè)Survivor區(qū),當(dāng)這個(gè)Survivor區(qū)滿時(shí),此區(qū)的存活對象將被復(fù)制到另外一個(gè)Survivor區(qū),當(dāng)?shù)诙€(gè)Survivor區(qū)也滿了的時(shí)候,從第一個(gè)Survivor區(qū)復(fù)制過來的并且此時(shí)還存活的對象,將被復(fù)制到老年代。2個(gè)Survivor是完全對稱,輪流替換。
Eden和2個(gè)Survivor的缺省比例是8:1:1,也就是10%的空間會被
浪費(fèi)??梢愿鶕?jù)GClog的信息調(diào)整大小的比例-
老年代(Old Generation)
- 存放了經(jīng)過一次或多次GC還存活的對象
- 一般采用Mark-Sweep或者M(jìn)ark-Compact算法進(jìn)行GC
- 有多種垃圾收集器可以選擇。每種垃圾收集器可以看作一個(gè)GC算法的具體實(shí)現(xiàn)??梢愿鶕?jù)具體應(yīng)用的需求選用合適的垃圾收集器(追求吞吐量?追求最短的響應(yīng)時(shí)間?)
-
永久代- 并不屬于堆(Heap).但是GC也會涉及到這個(gè)區(qū)域
- 存放了每個(gè)Class的結(jié)構(gòu)信息, 包括常量池、字段描述、方法描述。與垃圾收集要收集的Java對象關(guān)系不大
內(nèi)存分配與回收
堆上分配
大多數(shù)情況在eden上分配,偶爾會直接在old上分配細(xì)節(jié)取決于GC的實(shí)現(xiàn)棧上分配
原子類型的局部變量-
GC要做的是將那些dead的對象所占用的內(nèi)存回收掉
- Hotspot認(rèn)為沒有引用的對象是dead的
- Hotspot將引用分為四種: Strong、 Soft、Weak、Phantom
Strong 即默認(rèn)通過Object o=new Object()這種方式賦值的引用
Soft、Weak、 Phantom這 三種則都是繼承Reference
-
在Full GC時(shí)會對Reference類型的引用進(jìn)行特殊處理
- Soft:內(nèi)存不夠時(shí)一定會被GC、長期不用也會被GC
- Weak: - 定會被GC, 當(dāng)被mark為dead, 會在ReferenceQueue中通知
- Phantom: 本來就沒引用,當(dāng)從jvm heap中釋放時(shí)會通知
垃圾回收器

GC回收的時(shí)機(jī)
- 在分代模型的基礎(chǔ)上,GC從時(shí)機(jī)上分為兩種: Scavenge GC和Full GC
- Scavenge GC (Minor GC)
觸發(fā)時(shí)機(jī):新對象生成時(shí),Eden空間滿了理論上Eden區(qū)大多數(shù)對象會在ScavengeGC回收,復(fù)制算法的執(zhí)
行效率會很高,ScavengeGC時(shí)間比較短。 - Full GC
對整個(gè)JVM進(jìn)行整理,包括Young、Old 和Perm主要的觸發(fā)時(shí)機(jī): 1) Old滿了2) Perm滿了3) system.gc()效率很低,盡量減少Full GC。
- Scavenge GC (Minor GC)
垃圾回收器(Garbage Collector)
- 分代模型: GC的宏觀愿景;
- 垃圾回收器: GC的具體實(shí)現(xiàn)
- Hotspot JVM提供多種垃圾回收器,我們需要根據(jù)具體應(yīng)用的需要采用不同的回收器
- 沒有萬能的垃圾回收器,每種垃圾回收器都有自己的適用場景
垃圾收集器的‘并行”和并發(fā)
- 并行(Parallel):指多個(gè)收集器的線程同時(shí)工作,但是用戶線程處于等待狀態(tài)
- 并發(fā)(Concurrent):指收集器在工作的同時(shí),可以允許用戶線程工作。并發(fā)不代表解決了GC停頓的問題,在關(guān)鍵的步驟還是要停頓。比如在收集器標(biāo)記垃圾的時(shí)候。但在清除垃圾的時(shí)候,用戶線程可以和GC線程并發(fā)執(zhí)行。
Serial收集器
- 最早的收集器,單線程進(jìn)行GC, New和Old Generation都可以使用,在新生代,采用復(fù)制算法;
- 在老年代,采用Mark-Compact算法因?yàn)槭菃尉€程GC,沒有多線程切換的額外開銷,簡單實(shí)用
Hotspot Client模式默認(rèn)的收集器

ParNew收集器
ParNew收集器就是Serial的多線程版本,除了使用多個(gè)收集線程外,其余行為包括算法、STW、對象分配規(guī)則、回收策略等都與Seria收集器一模一樣。
對應(yīng)的這種收集器是虛擬機(jī)運(yùn)行在Server模式的默認(rèn)新生代收集器,在單CPU的環(huán)境中,ParNew收集器并不會比Serial收集器有更好的效果
Serial收集器在新生代的多線程版本
使用復(fù)制算法(因?yàn)獒槍π律?只有在多CPU的環(huán)境下,效率才會比Serial收集器高
可以通過-XX:ParallelGC Threads來控制GC線程數(shù)的多少。需要結(jié)合具體CPU的個(gè)數(shù)Server模式下新生代的缺省收集器

Parallel Scavenge收集器
- Parallel Scavenge收集器也是一個(gè)多線程收集器,也是使用復(fù)制算法,但它的對象分配規(guī)則與回收策略都與ParNew收集器有所不同,它是以吞吐量最大化(即GC時(shí)間占總運(yùn)行時(shí)間最小)為目標(biāo)的收集器實(shí)現(xiàn),它允許較長時(shí)間的STW換取總吞吐量最大化
CMS ( Concurrent Mark Sweep )收集器
CMS是一種以最短停頓時(shí)間為目標(biāo)的收集器,使用CMS并不能達(dá)到GC效率最高(總體GC時(shí)間最小),但它能盡可能降低GC時(shí)服務(wù)的停頓時(shí)間,CMS收集器使用的是標(biāo)記一清除算法
-
特點(diǎn):
- 追求最短停頓時(shí)間,非常適合Web應(yīng)用
- 只針對老年區(qū),一般結(jié)合ParNew使用
- Concurrent, GC線程和用戶線程并發(fā)工作(盡量并發(fā) )
- Mark-Sweep
- 只有在多CPU環(huán)境下才有意義
- 使用-XX:+UseConcMarkSweepGC打開
-
CMS收集器的缺點(diǎn)
- CMS以犧牲CPU資源的代價(jià)來減少用戶線程的停頓。當(dāng)CPU個(gè)數(shù)少于4的時(shí)候,有可能對吞吐量影響非常大
- CMS在并發(fā)清理的過程中,用戶線程還在跑。這時(shí)候需要預(yù)留一部分空間給用戶線程
- CMS用Mark-Sweep,會帶來碎片問題。碎片過多的時(shí)候會容易頻繁觸發(fā)FullGC

G1收集器

heap被劃分為一個(gè)個(gè)相等的不連續(xù)的內(nèi)存區(qū)域(regions) ,每個(gè)region都有一個(gè)分代的角色: eden、 survivor、 old
對每個(gè)角色的數(shù)量并沒有強(qiáng)制的限定,也就是說對每種分代內(nèi)存的大小,可以動態(tài)變化
G1最大的特點(diǎn)就是高效的執(zhí)行回收,優(yōu)先去執(zhí)行那些大量對象可回收的區(qū)域(region)
G1使用了gc停頓可預(yù)測的模型,來滿足用戶設(shè)定的gc停頓時(shí)間,根據(jù)用戶設(shè)定的目標(biāo)時(shí)間,G1會自動地選擇哪些region要清除,次清除多少個(gè)region
G1從多個(gè)region中復(fù)制存活的對象,然后集中放入一個(gè)region中,同時(shí)整理、清除內(nèi)存(copying收集算法)
對比使用mark-sweep的CMS, G1使用的copying算法不會造成內(nèi)存碎片;
對比Parallel Scavenge(基于copying )、Parallel Old收集器(基于mark-compact-sweep),Parallel會對整個(gè)區(qū)域做整理導(dǎo)致gc停頓會比較長,而G1只是特定地整理幾個(gè)region。
G1并非一個(gè)實(shí)時(shí)的收集器,與parallelScavenge-樣,對gc停頓時(shí)間的設(shè)置并不絕對生效,只是G1有較高的幾率保證不超過設(shè)定的gc停頓時(shí)間。與之前的gc收集器對比,G1會根據(jù)用戶設(shè)定的gc停頓時(shí)間,智能評估哪幾個(gè)region需要被回收可以滿足用戶的設(shè)定
分區(qū)(Region):
G1采取了不同的策略來解決并行、串行和CMS收集器的碎片、暫停時(shí)間不可控等問題一G1將 整個(gè)堆分成相同大小的分區(qū)(Region)
每個(gè)分區(qū)都可能是年輕代也可能是老年代,但是在同,時(shí)刻只能屬于某個(gè)代。年輕代、幸存區(qū)、老年代這些概念還存在,成為邏輯上的概念,這樣方便復(fù)用之前分代框架的邏輯。
在物理,上不需要連續(xù),則帶來了額外的好處有的分區(qū)內(nèi)垃圾對象特別多,有的分區(qū)內(nèi)垃圾對象很少,G1會優(yōu)先回收垃圾對象特別多的分區(qū),這樣可以花費(fèi)較少的時(shí)間來回收這些分區(qū)的垃圾,這也就是G1名字的由來,即首先收集垃圾最多的分區(qū)。
依然是在新生代滿了的時(shí)候,對整個(gè)新生代進(jìn)行回收整個(gè)新生代中的對象,要么被回收、要么晉升,至于新生代也采取分區(qū)機(jī)制的原因,則是因?yàn)檫@樣跟老年代的策略統(tǒng)一,方便調(diào)整代的大小
G1還是一種帶壓縮的收集器,在回收老年代的分區(qū)時(shí),是將存活的對象從一個(gè)分區(qū)拷貝到另一個(gè)可用分區(qū),這個(gè)拷貝的過程就實(shí)現(xiàn)了局部的壓縮。
收集集合(CSet)
- 一組可被回收的分區(qū)的集合。在CSet中存活的數(shù)據(jù)會在GC過程中被移動到另一個(gè)可用分區(qū),CSet中的分區(qū)可以來自eden空間、survivor空間、 或者老年代
已記憶集合(RSet) :
RSet記錄了其他Region中的對象引用本Region中對象的關(guān)系,屬于points-into結(jié)構(gòu)( 誰引用了我的對象)RSet的價(jià)值在于使得垃圾收集器不需要掃描整個(gè)堆找到誰引用了當(dāng)前分區(qū)中的對象,只需要掃描RSet即可。
Region1和Region3中的對象都引用了Region2中的對象,因此在Region2的RSet中記錄了這兩個(gè)引用。

G1 GC是在points-out的card table之上再加了一層結(jié)構(gòu)來構(gòu)成points-into RSet:每個(gè)region會記錄下到底哪些別的
region有指向自己的指針,而這些指針分別在哪些card的范圍內(nèi)。這個(gè)RSet其實(shí)是一個(gè)hash table,key是別的region的起始地址,value是一個(gè)集合,里面的元素是card table的index.
舉例來說,如果region A的RSet里有一項(xiàng)的key是region B,value里有index為1234的card,它的意思就是region B的
一個(gè)card里 有引用指向region A。所以對region A來說,該RSet記錄的是points-into的關(guān)系;而card table仍然記錄了points-out的關(guān)系。Snapshot-AtThe-Beginning(SATB):SATB是G1 GC在并發(fā)標(biāo)記階段使用的增量式的標(biāo)記算法,
并發(fā)標(biāo)記是并發(fā)多線程的,但并發(fā)線程在同一時(shí)刻只掃描一個(gè)分區(qū)
參考鏈接:https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
G1相對于CMS的優(yōu)勢
- G1在壓縮空間方面有優(yōu)勢
- G1通過將內(nèi)存空間分成區(qū)域(Region) 的方式避免內(nèi)存碎片問題Eden、Survivor、 Old區(qū)不再固定,在內(nèi)存使用效率上來說更靈活
- G1可以通過設(shè)置預(yù)期停頓時(shí)間( Pause Time) 來控制垃圾收集時(shí)間,避免應(yīng)用雪崩現(xiàn)象
- G1在回收內(nèi)存后會馬上同時(shí)做合并空閑內(nèi)存的工作,而CMS默認(rèn)是在STW ( stop the world) 的時(shí)候做
- G1會在Young GC中使用,而CMS只能在Old區(qū)使用
G1的適合場景
- 服務(wù)端多核CPU、JVM內(nèi)存占用較大的應(yīng)用
- 應(yīng)用在運(yùn)行過程中會產(chǎn)生大量內(nèi)存碎片、需要經(jīng)常壓縮空間
- 想要更可控、可預(yù)期的GC停頓周期:防止高并發(fā)下應(yīng)用的雪崩現(xiàn)象
G1 GC模式
G1提供了兩種GC模式,Young GC和Mixed GC, 兩種都是完全Stop The World的
Young GC:選定所有年輕代里的Region。通過控制年輕代的Region個(gè)數(shù),即年輕代內(nèi)存大小,來控制Young GC的時(shí)間開銷。
Mixed GC:選定所有年輕代里的Region,外加根據(jù)global concurrent marking統(tǒng)計(jì)得出收集收益高的若干老年代Region。在用戶指定的開銷目標(biāo)范圍內(nèi)盡可能選擇收益高的老年代Region
Mixed GC不是Full GC,它只能回收部分老年代的Region,如果Mixed GC實(shí)在無法跟上程序分配內(nèi)存的速度,導(dǎo)致老年代填滿無法繼續(xù)進(jìn)行MixedGC,就會使用serialold GC (Full GC)來收集整個(gè)GC heap。所以本質(zhì)上,G1是不提供Full GC的
global concurrent marking
初始標(biāo)記( initial mark, STW) :它標(biāo)記了從GCRoot開始直接可達(dá)的對象。
并發(fā)標(biāo)記( Concurrent Marking) :這個(gè)階段從GC Root開始對heap中的對象進(jìn)行標(biāo)記,標(biāo)記線
程與應(yīng)用程序線程并發(fā)執(zhí)行,并且收集各個(gè)Region的存活對象信息。重新標(biāo)記( Remark, STW) :標(biāo)記那些在并發(fā)標(biāo)記階段發(fā)生變化的對象,將被回收。
清理(Cleanup) :清除空Region (沒有存活對象的),加入到free list。
第一階段initial mark是共用了Young GC的暫停,這是因?yàn)樗麄兛梢詮?fù)用rootscan操作,所以可以說global concurrent marking是伴隨Young GC而發(fā)生的
第四階段Cleanup只是回收了沒有存活對象的Region,所以它并不需要STW。
G1在運(yùn)行過程中的主要模式
- YGC(不同于CMS)
- G1 YGC在Eden充滿時(shí)觸發(fā),在回收之后所有之前屬于Eden的區(qū)塊全部變成空白,即不屬于任何一個(gè)分區(qū)( Eden、Survivor、Old )
-
YGC執(zhí)行步驟:
- 階段1:根掃描
靜態(tài)和本地對象被描 - 階段2:更新RS
處理dirty card隊(duì)列更新RS - 階段3:處理RS
檢測從年輕代指向老年代的對象 - 階段4:對象拷貝
拷貝存活的對象到survivor/old區(qū)域 - 階段5:處理引用隊(duì)列
軟引用,弱引用,虛引用處理
- 階段1:根掃描
- 并發(fā)階段(global concurrent marking)
- 混合模式
- Full GC (一 般是G1出現(xiàn)問題時(shí)發(fā)生,本質(zhì)上不屬于G1,G1進(jìn)行的回退策略(回退為:Serial Old GC))
什么時(shí)候發(fā)生MixedGC?
- 由一些參數(shù)控制,另外也控制著哪些老年代Region會被選入CSet (收集集合)
- G1HeapWastePercent:在globalconcurrent marking結(jié)束之后,我們可以知道oldgenregions中有多少空間要被回收,在每次YGC之后和再次發(fā)生MixedGC之前,會檢查垃圾占比是否達(dá)到此參數(shù),只有達(dá)到了,下次才 會發(fā)生Mixed GC
- G1MixedGCLiveThresholdPercent: oldgeneration region中的存活對象的占比,只有在此參數(shù)之下,才會被選入CSet
- G1MixedGCCountTarget:一 次globalconcurrent marking之后,最多執(zhí)行Mixed GC的次數(shù)
- G1OldCSetRegionThresholdPercent:次Mixed GC中能被選入CSet的最多old generation region數(shù)量
三色標(biāo)記算法
提到并發(fā)標(biāo)記,我們不得不了解并發(fā)標(biāo)記的三色標(biāo)記算法。它是描述追蹤式回收器的一種有效的方法,利用它可以推演回收器的正確性
- 我們將對象分成三種類型:
- 黑色:根對象,或者該對象與它的子對象都被掃描過(對象被標(biāo)記了,且它的所有field也被標(biāo)記完了)
- 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象( 它的field還沒有被標(biāo)記或標(biāo)記完)
- 白色:未被掃描對象,掃描完成所有對象之后,最終為白色的為不可達(dá)對象,即垃圾對象(對象沒有被標(biāo)記到)
提到并發(fā)標(biāo)記,我們不得不了解并發(fā)標(biāo)記的三色標(biāo)記算法。它是描述追蹤式回收器的一種有效的方法,利用它可以推演回收器的正確性
遍歷了所有可達(dá)的對象后,所有可達(dá)的對象都變成了黑色。不可達(dá)的對象即為白色,需要被清理,如圖:

- 但是如果在標(biāo)記過程中,應(yīng)用程序也在運(yùn)行,那么對象的指針就有可能改變。這樣的話,我們就會遇到一個(gè)問題:對象丟失問題

這時(shí)候應(yīng)用程序執(zhí)行了以下操作:
A.c=C
B.c=null
這樣,對象的狀態(tài)圖變成如下情形:

這時(shí)候垃圾收集器再標(biāo)記掃描的時(shí)候就會變成下圖這樣

- 很顯然,此時(shí)C是白色,被認(rèn)為是垃圾需要清理掉,顯然這是不合理的
SATB
- 在G1中,使用的是SATB ( Snapshot-At-The- Beginning)的方式,刪除的時(shí)候記錄所有的對象
- 它有3個(gè)步驟
- 在開始標(biāo)記的時(shí)候生成一個(gè)快照圖,標(biāo)記存活對象
- 在并發(fā)標(biāo)記的時(shí)候所有被改變的對象入隊(duì)(在writebarrier里把所有舊的引用所指向的對象都變成非白的)
- 可能存在浮動垃圾,將在下次被收集
G1混合式回收
- G1到現(xiàn)在可以知道哪些老的分區(qū)可回收垃圾最多。當(dāng)全局并發(fā)標(biāo)記完成后,在某個(gè)時(shí)刻,就開始了Mixed GC。這些垃圾回收被稱作“混合式”是因?yàn)樗麄儾粌H僅進(jìn)行正常的新生代垃圾收集,同時(shí)也回收部分后臺掃描線程標(biāo)記的分區(qū)混合式GC也是采用的復(fù)制清理策略,當(dāng)GC完成后,會重新釋放空間
SATB詳解
- SATB是維持并發(fā)GC的一種手段。G1并發(fā)的基礎(chǔ)就是SATB。SATB可以理解成在GC開始之前對堆內(nèi)存里的對象做次快照,此時(shí)活的對象就認(rèn)為是活的,從而形成了一個(gè)對象圖。
- 在GC收集的時(shí)候,新生代的對象也認(rèn)為是活的對象,除此之外其他不可達(dá)的對象都認(rèn)為是垃圾對象
如何找到在GC過程中分配的對象呢?
每個(gè)region記錄著兩個(gè)top-at-mark-start ( TAMS 指針,分別為prevTAMS和nextTAMS。在TAMS以上的對象就是新分配的,因而被視為隱式marked。
通過這種方式我們就找到了在GC過程中新分配的對象,并把這些對象認(rèn)為是活的對象。
解決了對象在GC過程中分配的問題,那么在GC過程中引用發(fā)生變化的問題怎么解決呢?
G1給出的解決辦法是通過WriteBarrier.Write Barrier就是對引用字段進(jìn)行賦值做了額外處理。通過Write Barrier就可以了解到哪些引用對象發(fā)生了什么樣的變化
mark的過程就是遍歷heap標(biāo)記live object的過程,
采用的是三色標(biāo)記算法,這三種顏色為white(表示還未訪問到)、gray(訪問到但是它用到的引用還沒有完全掃描、black( 訪問到而且其用到的引用已經(jīng)完全掃描完)
整個(gè)三色標(biāo)記算法就是從GCroots出發(fā)遍歷heap,針對可達(dá)對象先標(biāo)記white為gray,然后再標(biāo)記gray為black;遍歷完成之后所有可達(dá)對象都是black的,所有white都是可以回收的
SATB僅僅對于在marking開始階段進(jìn)行"snapshot"(marked all reachable at markstart),但是concurrent的時(shí)候并發(fā)修改可能造成對象漏標(biāo)記
對black新引用了一個(gè)white對象,然后又從gray對象中刪除了對該white對象的引用,這樣會造成了該white對象漏標(biāo)記
對black新引用了一個(gè)white對象,然后從gray對象刪了一個(gè)引用該white對象的white對象,這樣也會造成了該white對象漏標(biāo)記,
對black新引用了一個(gè)剛new出來的white對象,沒有其他gray對象引用該white對象,這樣也會造成了該white對象漏標(biāo)記
對于三色算法在concurrent的時(shí)候可能產(chǎn)生的漏標(biāo)記問題,SATB在marking階段中,對于從gray對象移除的目標(biāo)引用對象標(biāo)記為gray,對于black引用的新產(chǎn)生的對象標(biāo)記為black;由于是在開始的時(shí)候進(jìn)行snapshot,因而可能存在Floating Garbage
漏標(biāo)與誤標(biāo)
誤標(biāo)沒什么關(guān)系,頂多造成浮動垃圾,在下次gc還是可以回收的,但是漏標(biāo)的后果是致命的,把本應(yīng)該存活的對象給回收了,從而影響的程序的正確性
-
漏標(biāo)的情況只會發(fā)生在白色對象中,且滿足以下任意一個(gè)條件
并發(fā)標(biāo)記時(shí),應(yīng)用線程給一個(gè)黑色對象的引用類型字段賦值 了該白色對象
并發(fā)標(biāo)記時(shí),應(yīng)用線程刪除所有灰色對象到該白色對象的引用
對于第一種情況,利用post-write barrier,記錄所有新增的引用關(guān)系,然后根據(jù)這些引用關(guān)系為根重新掃描一-遍
對于第二種情況,利用pre-write barrier,將所有即將被刪除的引用關(guān)系的舊引用記錄下來,最后以這些舊引用為根重新掃描一遍
停頓預(yù)測模型
G1收集器突出表現(xiàn)出來的一點(diǎn)是通過一個(gè)停頓預(yù)測模型根據(jù)用戶配置的停頓時(shí)間來選擇CSet的大小,從而達(dá)到用戶期待的應(yīng)用程序暫停時(shí)間。
通過-XX:MaxGCPauseMillis參數(shù)來設(shè)置。這一點(diǎn)有點(diǎn)類似于ParallelScavenge收集器。 關(guān)于停頓時(shí)間的設(shè)置并不是越短越好。
設(shè)置的時(shí)間越短意味著每次收集的CSet越小,導(dǎo)致垃圾逐步積累變多,最終不得不退化成SerialGC;停頓時(shí)間設(shè)置的過長,那么會導(dǎo)致每次都會產(chǎn)生長時(shí)間的停頓,影響了程序?qū)ν獾捻憫?yīng)時(shí)間
G1的收集模式
- G1的運(yùn)行過程是這樣的:會在Young GC和Mixed GC之間不斷地切換運(yùn)行,同時(shí)定期地做全局并發(fā)標(biāo)記,在實(shí)在趕不上對象創(chuàng)建速度的情況下 使用Full GC(Serial GC)。
- 初始標(biāo)記是在Young GC.上執(zhí)行的,在進(jìn)行全局并發(fā)標(biāo)記的時(shí)候不會做MixedGC,在做MixedGC的時(shí)候也不會啟動初始標(biāo)記階段。
- 當(dāng)MixedGC趕不上對象產(chǎn)生的速度的時(shí)候就退化成FullGC,這一點(diǎn)是需要重點(diǎn)調(diào)優(yōu)的地方
G1最佳實(shí)踐
不要設(shè)置新生代和老年代的大小,G1收集器在運(yùn)行的時(shí)候會調(diào)整新生代和老年代
的大小。通過改變代的大小來調(diào)整對象晉升的速度以及晉升年齡,從而達(dá)到我們?yōu)槭占髟O(shè)置的暫停時(shí)間目標(biāo)。設(shè)置了新生代大小相當(dāng)于放棄了G1為我們做的自動調(diào)優(yōu)。我們需要做的只是設(shè)置整個(gè)堆內(nèi)存的大小,剩下的交給G1自已去分配各個(gè)代的大小即可。
-
不斷調(diào)優(yōu)暫停時(shí)間指標(biāo)
- 通過-XX:MaxGCPauseMillis=x可以設(shè)置啟動應(yīng)用程序暫停的時(shí)間,G1在運(yùn)行的時(shí)候會根據(jù)這個(gè)參數(shù)選擇CSet來滿足響應(yīng)時(shí)間的設(shè)置。一般情況下這個(gè)值設(shè)置到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設(shè)置成50ms就不太合理。暫停時(shí)間設(shè)置的太短,就會導(dǎo)致出 現(xiàn)G1跟不上垃圾產(chǎn)生的速度。最終退化成Full GC。所以對這個(gè)參數(shù)的調(diào)優(yōu)是一個(gè)持續(xù)的過程,逐步調(diào)整到最佳狀態(tài)。
-
關(guān)注Evacuation Failure
- Evacuation(表示copy) Failure類似于CMS里面的晉升失敗,堆空間的垃圾太多導(dǎo)致無法完成Region之間的拷貝,于是不得不退化成Full GC來做一次全局范圍內(nèi)的垃圾收集
G1日志解析:
/**
*
* G1日志分析
* 虛擬機(jī)相關(guān)參數(shù):
* -verbose:gc
* -Xms10m
* -Xmx10m
* -XX:+UseG1GC 表示垃圾收集器使用G1
* -XX:+PrintGCDetails
* -XX:+PrintGCDateStamps
* -XX:MaxGCPauseMillis=200m 設(shè)置垃圾收集最大停頓時(shí)間
*/
public class G1LogAnalysis {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] bytes1 = new byte[size];
byte[] bytes2 = new byte[size];
byte[] bytes3 = new byte[size];
byte[] bytes4 = new byte[size];
System.out.println("hello world");
}
}
/**
* GC日志:
* 2020-04-14T16:13:41.663+0800: [GC pause (G1 Humongous Allocation【說明分配的對象超過了region大小的50%】) (young) (initial-mark), 0.0014516 secs]
* [Parallel Time: 1.1 ms, GC Workers: 4【GC工作線程數(shù)】]
* [GC Worker Start (ms): Min: 167.0, Avg: 167.1, Max: 167.1, Diff: 0.1]【幾個(gè)垃圾收集工作的相關(guān)信息統(tǒng)計(jì)】
* [Ext Root Scanning (ms): Min: 0.4, Avg: 0.4, Max: 0.4, Diff: 0.1, Sum: 1.6]
* [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
* [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
* [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
* [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
* [Object Copy (ms): Min: 0.6, Avg: 0.6, Max: 0.6, Diff: 0.0, Sum: 2.4]
* [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
* [Termination Attempts: Min: 1, Avg: 1.3, Max: 2, Diff: 1, Sum: 5]
* 【上面的幾個(gè)步驟為YOUNG GC的固定執(zhí)行步驟】
* 階段1:根掃描
* 靜態(tài)和本地對象被描
* 階段2:更新RS
* 處理dirty card隊(duì)列更新RS
* 階段3:處理RS
* 檢測從年輕代指向老年代的對象
* 階段4:對象拷貝
* 拷貝存活的對象到survivor/old區(qū)域
* 階段5:處理引用隊(duì)列
* 軟引用,弱引用,虛引用處理
* [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
* [GC Worker Total (ms): Min: 1.0, Avg: 1.1, Max: 1.1, Diff: 0.1, Sum: 4.2]
* [GC Worker End (ms): Min: 168.1, Avg: 168.1, Max: 168.1, Diff: 0.0]
* [Code Root Fixup: 0.0 ms]
* [Code Root Purge: 0.0 ms]
* [Clear CT: 0.1 ms]【清楚cardTable所花費(fèi)時(shí)間】
* [Other: 0.3 ms]
* [Choose CSet: 0.0 ms]
* [Ref Proc: 0.1 ms]
* [Ref Enq: 0.0 ms]
* [Redirty Cards: 0.1 ms]
* [Humongous Register: 0.0 ms]
* [Humongous Reclaim: 0.0 ms]
* [Free CSet: 0.0 ms]
* [Eden: 2048.0K(4096.0K)->0.0B【新生代清理后】(2048.0K) Survivors: 0.0B->1024.0K Heap: 3800.2K(10.0M)->2752.1K(10.0M)]
* [Times: user=0.00 sys=0.00, real=0.01 secs]
* 2020-04-14T16:13:41.671+0800: [GC concurrent-root-region-scan-start]
* 2020-04-14T16:13:41.671+0800: [GC concurrent-root-region-scan-end, 0.0008592 secs]
* 2020-04-14T16:13:41.671+0800: [GC concurrent-mark-start]
* 2020-04-14T16:13:41.672+0800: [GC concurrent-mark-end, 0.0000795 secs]
* 2020-04-14T16:13:41.672+0800: [GC remark 2020-04-14T16:13:41.672+0800: [Finalize Marking, 0.0001170 secs] 2020-04-14T16:13:41.672+0800: [GC ref-proc, 0.0002159 secs] 2020-04-14T16:13:41.672+0800: [Unloading, 0.0005800 secs], 0.0011024 secs]
* [Times: user=0.00 sys=0.00, real=0.00 secs]
* 2020-04-14T16:13:41.673+0800: [GC cleanup 4800K->4800K(10M), 0.0003239 secs]
* [Times: user=0.00 sys=0.00, real=0.00 secs]
* hello world
* Heap
* garbage-first heap total 10240K, used 4800K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
* region size 1024K【說明region默認(rèn)大小】, 2 young (2048K), 1 survivors (1024K)
* Metaspace used 3224K, capacity 4496K, committed 4864K, reserved 1056768K
* class space used 350K, capacity 388K, committed 512K, reserved 1048576K
*/