3.6 低延迟垃圾收集器
-
衡量垃圾收集器的三项最重要的指标:内存占用、吞吐量和延迟。
-
随着内存的扩大,对延迟的要求就会越来越高
-
在CMS和G1之前全部的收集器,其工作的所有步骤都会产生Stop The World式的停顿 。 而CMS和G1分别使用增量更新和原始快照实现了标记阶段的并发。
-
最后两款收集器Shenandoah和ZGC几乎整个工作过程都是并发的。 只有初始标记阶段和最终标记阶段有短暂的停顿,并且这部分时间基本都是固定的。
3.6.1 Shenandoah
- [ ,ʃenən’dəuə]
什么是Shenandoah收集器?
- 它可以说是G1的下一代继承者,有着相似的堆内存布局。也是使用基于Region的堆内存布局,同样有着用于存放大对象的Humongous Region,默认的回收策略也同样是优先处理回收价值最大的Region。
- Shenandoah维护记忆集的方式使用了"连接矩阵"的全局数据结构来记录跨Region的引用关系。
- 它与其他收集器的核心差异在于它的回收是并发的。 因为Region回收对象通过标记-复制算法,可同时工作线程也在运行,如果移动之后,用户线程访问到旧的地址,就会出错。
什么是连接矩阵?
- 可以理解成一张二维表格,如果Region N 有对象指向Region M ,就在表格的N行M列中打上一个标记。通过这个表格就可以得出哪些Region之间有跨Region的引用。
如何并发回收?
- 在Shenandoah收集器中采用了转发指针,在每一个对象头前面增加一个新的引用字段,该引用指向对象自己。 不过每次对象访问都会带来一次额外的转向开销。
3.6.2 ZGC收集器
- 目标是在尽可能对吞吐量影响不大的情况下,实现在任意堆内存大小都可以把垃圾收集的停顿时间限制在10毫秒以内的低延迟。
- ZGC收集器是一款基于Region内存布局的,不设置分代的 , 使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的 , 以低延迟为首要目标的一款收集器。
- ZGC的Region官方称为Page,是具有动态性的,可以动态创建和销毁,以及动态的 区域容量大小。
如何实现并发整理算法?
- 采用了染色指针算法,它将标记信息直接记录在引用对象指针上面。它是一种将少量额外信息存放在指针上的技术。(只有引用关系才能决定对象存活与否)
染色指针算法的好处?
- 可以使得一旦某个Region的存活对象被移走之后,这个Region可以立刻被释放或者重用掉 。 不需要等待整个堆中所有指向该Region的引用都被修正后才能清理。
- 可以大幅度减少在垃圾收集过程中内存屏障的使用数量。设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变化情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。
- 染色指针可以作为一种可拓展的存储结构用来记录更多与对象标记、重定位过程相关的数据
介绍下ZGC收集器的运作过程?
- 并发标记:遍历对象图进行可达性分析
- 并发预备重分配:根据特定的查询条件统计得出本次收集过程需要清理的Region,将这些Region组成重分配集。(G1的回收集是做收益优先的增量回收)
- 并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集 中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
- 并发重映射:修正整个堆中指向重分配集 中旧对象的所有引用。
3.7 内存分配
- Java技术体系的最根本的目标是自动化解决两个问题:自动给对象分配内存 以及 自动回收分配给对象的内存。
- 对象大多数情况下,会在新生代Eden区中分配。 当新生代空间满的时候,会发生一次Minor GC。
public class testAll {
public static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
testAllocation();
}
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]; //出现一次Minor GC
//通过参数设置Java堆的大小为20MB,其中新生代为10MB
//因为Eden区和Survivor区的比例是8:1 , 所以新生代总可用为9MB。(只用其中一个Eden)
//为allocation4分配内存时,发现Eden空间放不下了,所以会发生一次Minor GC
//这一次GC将6MB的对象放入到老年代。(这几个对象都存活)
}
}
- 大对象直接进入老年代
- 大对象就是指需要大量连续内存空间的Java对象,最典型的就是很长的字符串或者元素数量很庞大的数组。
- HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配
public static void testPretenureSizeThreshold(){
byte[] allocation;
allocation = new byte[4 * _1MB]; //直接分配在老年代
}
- 长期存活的对象将进入老年代
- 虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头中。
- 对象经过第一次Minor GC还存活,并且能被Survivor容纳的话,会进入到Survivor空间,并将其年龄设为1。
- 默认为15岁,就会晋升到老年代。 -XX:MaxTenuringThreshold设置
//VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
// -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
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];
}
- 动态对象年龄判定
- HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升到老年代
- 如果在Survivor空间中低于或等于某年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄(这里的年龄指的是前面的某年龄)的所有对象就可以直接进入老年代。
- 空间分配担保
- 在发生Minor GC之前,虚拟机必须先检查老年代 最大可用的连续空间是否大于 新生代所有对象总空间。
- 如果这个条件成立,说明这一次Minor GC可以确保安全。
- 如果不成立,会查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败。 如果允许,会检查老年代最大可用的连续空间是否 大于 历次晋升到老年代对象的平均大小,如果大于尝试一次Minor GC。 如果小于或者参数不允许,那么就要进行一次Full GC。