导语
G1回收器是在JDK1.7中正式使用的一种全新的垃圾回收器,它的目标是为了取代CMS回收器。G1回收器拥有独特的垃圾回收策略,和之前的任意的一种垃圾回收器都有所不同,但是从分代策略上来说依然是属于分代垃圾回收器,也分为年轻代和老年代,任然是有eden区和survivor区。但从堆结构上看,它并不要求整个的eden区、年轻代或者老年代是连续的空间。它所使用的是一个分区算法,这个后面的分享中会介绍到。
作为CMS的替代算法,G1使用了全新的分区算法特点如下
并行性 :G1在回收期间,可以由多个GC线程同时工作,有效的利用多核的计算能力
并发性 :G1拥有与应用程序交替执行的能力,部分工作可以与应用程序同时执行,所以,不会在一整个回收期间完全阻塞应用程序。
分代GC:G1 是一个分代收集器,与之前的收集器不同的是它兼顾了年轻代和老年代,相比较于其他的垃圾回收器,其他的收集器只能工作于年轻代或者老年代其中的一个,所以在这个方面和之前的收集器有所不同。
空间整理:G1 在回收过程中会进行适当的对象移动,与CMS不同,只是简单的标记清理对象,在若干次GC之后,CMS必须执行一次碎片整理,G1则是每次回收都会有效的复制对象,尽量减少空间碎片。
可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,可以缩小回收范围,对于全局停顿有很好的掌控性。
选项/默认值 | 说明 |
---|---|
-XX:+UseG1GC | 使用 G1 (Garbage First) 垃圾收集器 |
-XX:MaxGCPauseMillis=n | 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标. |
-XX:InitiatingHeapOccupancyPercent=n | 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认值为 45. |
-XX:NewRatio=n | 新生代与老生代(new/old generation)的大小比例(Ratio). 默认值为 2. |
-XX:SurvivorRatio=n | eden/survivor 空间大小的比例(Ratio). 默认值为 8. |
-XX:MaxTenuringThreshold=n | 提升年老代的最大临界值(tenuring threshold). 默认值为 15. |
-XX:ParallelGCThreads=n | 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同. |
-XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同. |
-XX:G1ReservePercent=n | 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10. |
-XX:G1HeapRegionSize=n | 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb. |
G1收集器将堆进行分区,将内存分为不同的区域,每次回收的时候,只回收其中的几个区域,这样可以有效的来控制垃圾回收产生的一次停顿时间。
G1 的收集过程有以下的4个阶段
新生代的GC主要工作是回收eden区和survivor区。一旦eden区被占用满,新生代GC就会启动。如下图所示
其中E表示eden区,S表示survivor区,O表示老年代。可以看出来在新生代GC只处理eden区和survivor区,回收之后所有的Eden区都应该是被清空,survivor去被回收部分数据,但至少会存在一个survivor区。与其他的收集器来讲这点的变化不会太大。另外的变化就是老年代的区域会增加,这是因为部分survivor区或者是eden区的对象可能会升级到老年代。
-Xmx1g -Xms1g -Xmn900m -XX:+UseG1GC -Xloggc:gc.log -XX:+PrintGCDetails
观察日志情况会看到与之前的垃圾回收日志信息有所不同
10.519: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0359476 secs]
[Parallel Time: 34.8 ms, GC Workers: 13]
[GC Worker Start (ms): Min: 10519.2, Avg: 10519.3, Max: 10519.4, Diff: 0.2]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 2.8]
[Update RS (ms): Min: 1.2, Avg: 1.3, Max: 1.5, Diff: 0.4, Sum: 17.0]
[Processed Buffers: Min: 2, Avg: 2.1, Max: 3, Diff: 1, Sum: 27]
[Scan RS (ms): Min: 3.6, Avg: 3.7, Max: 3.8, Diff: 0.2, Sum: 48.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 29.3, Avg: 29.3, Max: 29.4, Diff: 0.1, Sum: 381.2]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.9, Max: 3, Diff: 2, Sum: 25]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
[GC Worker Total (ms): Min: 34.5, Avg: 34.6, Max: 34.7, Diff: 0.2, Sum: 449.4]
[GC Worker End (ms): Min: 10553.8, Avg: 10553.8, Max: 10553.9, Diff: 0.1]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.2 ms]
[Other: 0.9 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.2 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.3 ms]
[Eden: 72.0M(69.0M)->0.0B(107.0M) Survivors: 8192.0K->10.0M Heap: 458.0M(800.0M)->461.5M(800.0M)]
[Times: user=0.17 sys=0.19, real=0.04 secs]
10.555: [GC concurrent-root-region-scan-start]
10.556: [GC concurrent-root-region-scan-end, 0.0010221 secs]
10.556: [GC concurrent-mark-start]
10.655: [GC concurrent-mark-end, 0.0992927 secs]
10.656: [GC remark 10.656: [Finalize Marking, 0.0012177 secs] 10.657: [GC ref-proc, 0.0000598 secs] 10.657: [Unloading, 0.0004519 secs], 0.0036237 secs]
[Times: user=0.02 sys=0.00, real=0.00 secs]
10.659: [GC cleanup 466M->462M(800M), 0.0013445 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]
10.661: [GC concurrent-cleanup-start]
10.661: [GC concurrent-cleanup-end, 0.0000085 secs]
和其他收集器的日志相比G1的日志内容比较丰富,从日志中可以看到,Eden区的使用情况,以及Survivors的使用情况。
[Eden: 900.0M(900.0M)->0.0B(796.0M) Survivors: 0.0B->104.0M Heap: 915.5M(1024.0M)->452.5M(1024.0M)]
G1并发阶段与CMS类似,都是为了降低一次的停顿时间,可以将与应用程序一起执行的部分提取然后执行。主要可以分为如下的步骤
如图所示,在并发标记周期前堆内存使用情况,由于并发标记周期包含一次新生代的GC,所以新生代会进行整理,但由于并发标记执行时,程序依然处于运行期,所以并发标记之后又有新的eden空间被使用。
并发标记周期后
并发标记周期执行前后最大的不同是在新生代GC之后,系统会增加一些标记为G的区域,这些区域被标记,因为它们内部的垃圾比例较高,所以在之后的混合GC中进行收集(在并发标记周期中并没有正式回收这些区域)。这些将要被回收的区域会被G1记录在Collection Sets的集合中,在之前的日志中可以看到。
1、初始标记 伴随一次新生代的GC
10.519: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0359476 secs]
[Eden: 72.0M(69.0M)->0.0B(107.0M) Survivors: 8192.0K->10.0M Heap: 458.0M(800.0M)->461.5M(800.0M)]
可以看到在初始化,eden区域被清空。
2、并发扫描根区域
10.555: [GC concurrent-root-region-scan-start]
10.556: [GC concurrent-root-region-scan-end, 0.0010221 secs]
3、并发标记
10.556: [GC concurrent-mark-start]
10.655: [GC concurrent-mark-end, 0.0992927 secs]
4、重新标记
会引起全局停顿
10.656: [GC remark 10.656: [Finalize Marking, 0.0012177 secs] 10.657: [GC ref-proc, 0.0000598 secs] 10.657: [Unloading, 0.0004519 secs], 0.0036237 secs]
5、独占清理
独占清理会重新计算各个区域的存活对象,并以此可以得到每个区域进行回收的比例。
10.659: [GC cleanup 466M->462M(800M), 0.0013445 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]
6、并发清理
根据独占清理阶段计算得出的每个区域的存活对象数量,直接回收已经不包含存活对象的区域
10.661: [GC concurrent-cleanup-start]
10.661: [GC concurrent-cleanup-end, 0.0000085 secs]
测试代码
public class StopWorldTest {
public static class MyThread extends Thread{
HashMap map = new HashMap();
@Override
public void run() {
try{
while (true){
if (map.size()*512/1024/1024>=500){
map.clear();
System.out.println("clean map");
}
byte[] b1 ;
for (int i =0;i<100;i++){
b1 = new byte[512];
map.put(System.nanoTime(),b1);
}
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static class PrintThread extends Thread{
public static final long starttime = System.currentTimeMillis();
@Override
public void run() {
try {
while (true){
long t = System.currentTimeMillis()-starttime;
System.out.println(t/1000+"."+t%1000);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
PrintThread p = new PrintThread();
t.setName("MyThread");
p.setName("PrintThread");
t.start();
p.start();
}
}
虚拟机参数
-Xmx800m -Xms800m -XX:+UseG1GC -Xloggc:gc.log -XX:+PrintGCDetails
在上面的并发标记周期中,虽然是有一部分对象被回收,但是从总体上来讲,回收的比率是比较低的。并发标记带来的最大的好处就是G1可以明确知道哪些区域所包含的垃圾较多,在混合回收的时候就可以专门对这些区域进行回收。当然G1会优先回收垃圾比例较高的区域,因为这些区域的回收性价比比较高。这就G1( Garbage First Garbage Collector)
这个整合的阶段被称为是混合回收,因为在这个阶段会执行正常的年轻代GC,又选择性的回收一部分被标记的老年代,同时处理老年代与新生代。如图所示混合回收前。
混合回收之后
会看到由于新生代GC毕然导致Eden被清空,而标记G的区域就变为了垃圾比例最高的区域,所以被清理。
17.698: [GC pause (G1 Evacuation Pause) (mixed), 0.0119168 secs]
[Eden: 34.0M(34.0M)->0.0B(259.0M) Survivors: 6144.0K->5120.0K Heap: 208.0M(800.0M)->201.0M(800.0M)]
混合GC会执行多次,知道回收了足够多的内存,然后就会触发一次新生代GC,新生代GC之后有可能会执行一次并发标记周期,最后又会引起混合GC。
在有些场景下会在日志里面出现了Full GC 的信息。这是因为并发收集由于让应用程序和GC线程交替工作,所以总是不能避免在特别繁忙的时候回出现回收的过程中内存不足的情况,这个时候就会进行一次的Full GC,可以使用如下的参数作为测试参数查看对应的效果
-Xmx1g -Xms1g -Xmn900m -XX:+UseG1GC -Xloggc:gc.log -XX:+PrintGCDetails
对于G1回收器,可以通过-XX:+UseG1GC 标记来打开,可以通过-XX:+MaxGCPauseMillis 用于指定目标最大停顿时间,如果任何一次的停顿大小与这个时间G1就会尝试重新调整新生代和老年的比例、调整整个堆大小、调整晋升年龄等。通过-XX:+ParallelGCThreads 设置并行回收的时候,GC工作线程数量,-XX:InitiatingHeapOccupancyPercent参数可以设置整个堆使用率达到多少的时候触发标记周期执行,默认是45。整个值一旦被设置就不会被修改,这个值配合MaxGCPauseMillis 来使用。当然还有很多的性能调优的参数,这里就不再一一介绍。