Java虚拟机——低延迟垃圾收集器&内存分配

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,是具有动态性的,可以动态创建和销毁,以及动态的 区域容量大小。

如何实现并发整理算法?

  • 采用了染色指针算法,它将标记信息直接记录在引用对象指针上面。它是一种将少量额外信息存放在指针上的技术。(只有引用关系才能决定对象存活与否)

染色指针算法的好处?

  1. 可以使得一旦某个Region的存活对象被移走之后,这个Region可以立刻被释放或者重用掉 。 不需要等待整个堆中所有指向该Region的引用都被修正后才能清理。
  2. 可以大幅度减少在垃圾收集过程中内存屏障的使用数量。设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变化情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。
  3. 染色指针可以作为一种可拓展的存储结构用来记录更多与对象标记、重定位过程相关的数据

介绍下ZGC收集器的运作过程?

  1. 并发标记:遍历对象图进行可达性分析
  2. 并发预备重分配:根据特定的查询条件统计得出本次收集过程需要清理的Region,将这些Region组成重分配集。(G1的回收集是做收益优先的增量回收)
  3. 并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集 中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
  4. 并发重映射:修正整个堆中指向重分配集 中旧对象的所有引用。

3.7 内存分配

  • Java技术体系的最根本的目标是自动化解决两个问题:自动给对象分配内存 以及 自动回收分配给对象的内存。
  1. 对象大多数情况下,会在新生代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的对象放入到老年代。(这几个对象都存活)
    }
}
  1. 大对象直接进入老年代
  • 大对象就是指需要大量连续内存空间的Java对象,最典型的就是很长的字符串或者元素数量很庞大的数组。
  • HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配
public static void testPretenureSizeThreshold(){
    byte[] allocation;
    allocation = new byte[4 * _1MB];  //直接分配在老年代
}
  1. 长期存活的对象将进入老年代
  • 虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头中。
  • 对象经过第一次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];
}
  1. 动态对象年龄判定
  • HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升到老年代
  • 如果在Survivor空间中低于或等于某年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄(这里的年龄指的是前面的某年龄)的所有对象就可以直接进入老年代。
  1. 空间分配担保
  • 在发生Minor GC之前,虚拟机必须先检查老年代 最大可用的连续空间是否大于 新生代所有对象总空间。
  • 如果这个条件成立,说明这一次Minor GC可以确保安全。
  • 如果不成立,会查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败。 如果允许,会检查老年代最大可用的连续空间是否 大于 历次晋升到老年代对象的平均大小,如果大于尝试一次Minor GC。 如果小于或者参数不允许,那么就要进行一次Full GC。

你可能感兴趣的:(Java虚拟机,java,开发语言,算法)