9.7 G1收集器
可以在新生代和老年代中只使用G1收集器。具有如下特点。
1. 并行和并发。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。
2. 分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。
3. 空间整合。基于标记 - 整理算法,无内存碎片产生。
4. 可预测的停顿。能简历可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
使用G1收集器时,Java堆会被划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但两者已经不是物理隔离了,都是一部分Region(不需要连续)的集合。G1收集器中,Region之间的对象引用以及其他收集器的新生代和老年代之间的对象引用,虚拟机都使用Remembered Set来避免全堆扫描的。每个Region对应一个Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查老年代的对象是否引用了新生代的对象),如果是,则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
对于上述过程我们可以看如下代码加深理解
public class G1 { private Object obj; public init() { obj = new Object(); } public static void main(String[] args) { G1 g1 = new G1(); g1.init(); } }
说明:程序中执行init函数的时候,会产生一个Write Barrier暂停中断写操作,此时,假定程序中G1对象与Object对象被分配在不同的Region当中,则会把obj的引用信息记录在Object所属的Remembered Set当中。具体的内存分布图如下
如果不计算维护Remembered Set的操作,G1收集器的运作可以分为如下几步
1. 初始并发,标记GCRoots能直接关联到的对象;修改TAMS(Next Top At Mark Start),使得下一阶段程序并发时,能够在可用的Region中创建新对象,需停顿线程,耗时很短。
2. 并发标记,从GCRoots开始进行可达性分析,与用户程序并发执行,耗时很长。
3. 最终标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,变动的记录将被记录在Remembered Set Logs中,此阶段会把其整合到Remembered Set中,需要停顿线程,与用户程序并行执行,耗时较短。
4. 筛选回收,对各个Region的回收价值和成本进行排序,根据用户期望的GC时间进行回收,与用户程序并发执行,时间用户可控。
G1收集器具体的运行示意图如下
各个垃圾回收器的介绍就到这里,有兴趣的读者可以去阅读源码。
看到这里,相信有些读者对之前的GC日志可能会有些疑惑,下面我们来理解一下GC日志
[GC (System.gc()) [PSYoungGen: 6270K->584K(9216K)] 11390K->5712K(19456K), 0.0011969 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 584K->0K(9216K)] [ParOldGen: 5128K->514K(10240K)] 5712K->514K(19456K), [Metaspace: 2560K->2560K(1056768K)], 0.0059342 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 9216K, used 82K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 1% used [0x00000000ff600000,0x00000000ff614920,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 514K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 5% used [0x00000000fec00000,0x00000000fec80928,0x00000000ff600000)
Metaspace used 2567K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 275K, capacity 386K, committed 512K, reserved 1048576K
这是之前出现过的GC日志,可以知道笔者虚拟机的垃圾收集器的组合为Parallel Scavenge(新生代) + Parallel Old(老年代),是根据PSYoungGen和ParOldGen得知,不同的垃圾回收器的不同组成的新生代和老年代的名字也有所不同。虚拟机也提供了参数供我们选择不同的垃圾收集器。
1. [GC (System.gc())]与[Full GC (System.gc())],说明垃圾收集的停顿类型,不是区分新生代GC和老年代GC的,如果有Full,则表示此次GC发生了Stop The World。
2. PSYoungGen: 6270K->584K(9216K),表示,新生代:该内存区域GC前已使用容量 -> 该内存区域GC后已使用容量(该内存区域总容量)
3. 11390K->5712K(19456K),表示,GC前Java堆已使用的容量 -> GC后Java堆已使用的容量(Java堆总容量)
4. 0.0011969 secs,表示GC所占用的时间,单位为秒。
5. [Times: user=0.00 sys=0.00, real=0.00 secs],表示GC的更具体的时间,user代表用户态消耗的CPU时间,sys代表内核态消耗的CPU时间,real代表操作从开始到结束所经过的墙钟时间。CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,如等待磁盘IO,等待线程阻塞,CPU时间则不包含这些耗时。当系统有多CPU或者多核时,多线程操作会叠加这些CPU时间,所以读者看到user或者sys时间超过real时间也是很正常的。
十、内存分配与回收策略
前面我们已经详细讨论了内存回收,但是,我们程序中生成的对象是如何进行分配的呢?对象的内存分配,绝大部分都是在堆上分配,少数经过JIT编译后被拆散为标量类型并间接在栈上分配。在堆上的分配又可以有如下分配,主要在新生代的Eden区分配,如果启动了本地线程分配缓冲,将按照线程优先在TLAB上分配,少数直接在老年代分配,虚拟机也提供了一些参数供我们来控制对象内存空间的分配。
堆的结构图如下图所示
下面我们将从应用程序的角度理解对象的分配。
10.1 对象优先在Eden区分配
对象通常在新生代的Eden区进行分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,与Minor GC对应的是Major GC、Full GC。
Minor GC:指发生在新生代的垃圾收集动作,非常频繁,速度较快。
Major GC:指发生在老年代的GC,出现Major GC,经常会伴随一次Minor GC,同时Minor GC也会引起Major GC,一般在GC日志中统称为GC,不频繁。
Full GC:指发生在老年代和新生代的GC,速度很慢,需要Stop The World。
如下代码片段展示了GC的过程
public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC * */ public static void testAllocation() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { testAllocation(); } }
运行结果:
[GC (Allocation Failure) [DefNew: 7130K->515K(9216K), 0.0048317 secs] 7130K->6659K(19456K), 0.0048809 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 4694K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
from space 1024K, 50% used [0x00000000ff500000, 0x00000000ff580fa0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
Metaspace used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 275K, capacity 386K, committed 512K, reserved 1048576K
说明:新生代可用的空间为9M = 8M(Eden容量) + 1M(一个survivor容量),分配完allocation1、allocation2、allocation3之后,无法再分配allocation4,会发生分配失败,则需要进行一次Minor GC,survivor to区域的容量为1M,无法容纳总量为6M的三个对象,则会通过担保机制将allocation1、allocation2、allocation3转移到老年代,然后再将allocation4分配在Eden区。
10.2 大对象直接进入老年代
需要大量连续内存空间的Java对象称为大对象,大对象的出现会导致提前触发垃圾收集以获取更大的连续的空间来进行大对象的分配。虚拟机提供了-XX:PretenureSizeThreadshold参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。
具体代码如下
public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728(3M) * */ public static void testPretenureSizeThreshold() { byte[] allocation4 = new byte[5 * _1MB]; } public static void main(String[] args) { testPretenureSizeThreshold(); } }
运行结果:
Heap
def new generation total 9216K, used 1314K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 16% used [0x00000000fec00000, 0x00000000fed489d0, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 5120K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 50% used [0x00000000ff600000, 0x00000000ffb00010, 0x00000000ffb00200, 0x0000000100000000)
Metaspace used 2567K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 275K, capacity 386K, committed 512K, reserved 1048576K
说明:可以看到5MB的对象直接分配在了老年代。
10.3 长期存活的对象进入老年代
每个对象有一个对象年龄计数器,与前面的对象的存储布局中的GC分代年龄对应。对象出生在Eden区、经过一次Minor GC后仍然存活,并能够被Survivor容纳,设置年龄为1,对象在Survivor区每次经过一次Minor GC,年龄就加1,当年龄达到一定程度(默认15),就晋升到老年代,虚拟机提供了-XX:MaxTenuringThreshold来进行设置。
具体代码如下
public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution * */ public static void testTenuringThreshold() { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; } public static void main(String[] args) { testPretenureSizeThreshold(); } }
运行结果:
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 790400 bytes, 790400 total
: 5174K->771K(9216K), 0.0050541 secs] 5174K->4867K(19456K), 0.0051088 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 4867K->0K(9216K), 0.0015279 secs] 8963K->4867K(19456K), 0.0016327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 4867K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 47% used [0x00000000ff600000, 0x00000000ffac0d30, 0x00000000ffac0e00, 0x0000000100000000)
Metaspace used 2562K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 275K, capacity 386K, committed 512K, reserved 1048576K
说明:发生了两次Minor GC,第一次是在给allocation3进行分配的时候会出现一次Minor GC,此时survivor区域不能容纳allocation2,但是可以容纳allocation1,所以allocation1将会进入survivor区域并且年龄为1,达到了阈值,将在下一次GC时晋升到老年代,而allocation2则会通过担保机制进入老年代。第二次发生GC是在第二次给allocation3分配空间时,这时,allocation1的年龄加1,晋升到老年代,此次GC也可以清理出原来allocation3占据的4MB空间,将allocation3分配在Eden区。所以,最后的结果是allocation1、allocation2在老年代,allocation3在Eden区。
10.4 动态对象年龄判断
对象的年龄到达了MaxTenuringThreshold可以进入老年代,同时,如果在survivor区中相同年龄所有对象大小的总和大于survivor区的一半,年龄大于等于该年龄的对象就可以直接进入老年代。无需等到MaxTenuringThreshold中要求的年龄。
具体代码如下:
public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution * */ public static void testTenuringThreshold2() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { testPretenureSizeThreshold2(); } }
运行结果:
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 1048576 bytes, 1048576 total
: 5758K->1024K(9216K), 0.0049451 secs] 5758K->5123K(19456K), 0.0049968 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 5120K->0K(9216K), 0.0016442 secs] 9219K->5123K(19456K), 0.0016746 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 5123K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 50% used [0x00000000ff600000, 0x00000000ffb00f80, 0x00000000ffb01000, 0x0000000100000000)
Metaspace used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 275K, capacity 386K, committed 512K, reserved 1048576K
结果说明:发生了两次Minor GC,第一次发生在给allocation4分配内存时,此时allocation1、allocation2将会进入survivor区,而allocation3通过担保机制将会进入老年代。第二次发生在给allocation4分配内存时,此时,survivor区的allocation1、allocation2达到了survivor区容量的一半,将会进入老年代,此次GC可以清理出allocation4原来的4MB空间,并将allocation4分配在Eden区。最终,allocation1、allocation2、allocation3在老年代,allocation4在Eden区。
10.5 空间分配担保
在发生Minor GC时,虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和,若成立,则说明Minor GC是安全的,否则,虚拟机需要查看HandlePromotionFailure的值,看是否运行担保失败,若允许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次Minor GC;若小于或者HandlePromotionFailure设置不运行冒险,那么此时将改成一次Full GC,以上是JDK Update 24之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
冒险是指经过一次Minor GC后有大量对象存活,而新生代的survivor区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,把survivor区无法容纳的对象直接进入老年代。
具体代码如下:
public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+HandlePromotionFailure * */ public static void testHandlePromotion() { byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7, allocation8; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation1 = null; allocation4 = new byte[2 * _1MB]; allocation5 = new byte[2 * _1MB]; allocation6 = new byte[2 * _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 * _1MB]; } public static void main(String[] args) { testHandlePromotion(); } }
运行结果:
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 528280 bytes, 528280 total
: 7294K->515K(9216K), 0.0040766 secs] 7294K->4611K(19456K), 0.0041309 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 6818K->0K(9216K), 0.0012444 secs] 10914K->4611K(19456K), 0.0012760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 4611K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 45% used [0x00000000ff600000, 0x00000000ffa80d58, 0x00000000ffa80e00, 0x0000000100000000)
Metaspace used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 275K, capacity 386K, committed 512K, reserved 1048576K
说明:发生了两次GC,第一次发生在给allocation4分配内存空间时,由于老年代的连续可用空间大于存活的对象总和,所以allocation2、allocation3将会进入老年代,allocation1的空间将被回收,allocation4分配在新生代;第二次发生在给allocation7分配内存空间时,此次GC将allocation4、allocation5、allocation6所占的内存全部回收。最后,allocation2、allocation3在老年代,allocation7在新生代。
十一、总结
至此,JVM垃圾收集部分就已经介绍完了,看完这部分我们应该知道JVM是怎样进行垃圾回收的,并且对JVM的理解更加加深。
花了很长时间,终于写完了这一部分,还是收获很多,在看的同时不断记录,更进一步加深了印象,感觉还不错,谢谢各位园友的观看~