JVM-GC-ZGC

  • ZGC中的内存布局
  • ZGC流程
  • ZGC参数设置

ZGC中的内存布局

  • ZGC 设计目标:ZGC(The Z Garbage Collector)是JDK 11中推出的一款追求极致低延迟的垃圾收集器
    • 停顿时间不超过10ms(JDK16已经达到不超过1ms)
    • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加
    • 支持8MB~4TB级别的堆,JDK15后已经可以支持16TB
  • ZGC中的内存布局
    • 为了细粒度地控制内存的分配,和G1一样,ZGC将内存划分成小的分区,在ZGC中称为页面(page)

    • ZGC中没有分代的概念(新生代、老年代)

    • ZGC支持3种页面,分别为小页面、中页面和大页面

    • 小页面指的是2MB的页面空间,中页面指32MB的页面空间,大页面指受操作系统控制的大页

      • 当对象大小小于等于256KB时,对象分配在小页面
      • 当对象大小在256KB和4M之间,对象分配在中页面
      • 当对象大于4M,对象分配在大页面
    • 回收策略:小页面优先回收;中页面和大页面则尽量不回收

    • 为什么这么设计

      • 标准大页(huge page)是Linux Kernel 2.6引入的,目的是通过使用大页内存来取代传统的4KB内存页面,以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能
      • Huge pages 有两种格式大小: 2MB 和 1GB , 2MB 页块大小适合用于 GB 大小的内存, 1GB 页块大小适合用于 TB 级别的内存; 2MB 是默认的页大小
  • ZGC支持NUMA
      • 在过去,对于X86架构的计算机,内存控制器还没有整合进CPU,所有对内存的访问都需要通过北桥芯片来完 成。X86系统中的所有内存都可以通过CPU进行同等访问。任何CPU访问任何内存的速度是一致的,不必考虑不同内存地址之间的差异,这称为“统一内存访问”(Uniform Memory Access,UMA)
      • 在UMA中,各处理器与内存单元通过互联总线进行连接,各个CPU之间没有主从关系
      • 之后的X86平台经历了一场从“拼频率”到“拼核心数”的转变,越来越多的核心被尽可能地塞进了同一块芯片上,各个核心对于内存带宽的争抢访问成为瓶颈,所以人们希望能够把CPU和内存集成在一个单元上(称Socket),这就是非统一内存访问(Non-Uniform Memory Access,NUMA)。很明显,在NUMA下,CPU访问本地存储器的速度比访问非本地存储器快一些
      • ZGC是支持NUMA的,在进行小页面分配时会优先从本地内存分配,当不能分配时才会从远端的内存分配
      • 对于中页面和大页面的分配,ZGC并没有要求从本地内存分配,而是直接交给操作系统,由操作系统找到一块能满足ZGC页面的空间
      • 对于小页面,存放的都是小对象,从本地内存分配速度很快,且不会造成内存使用的不平衡,而中页面和大页面因为需要的空间大,如果也优先从本地内存分配,极易造成内存使用不均衡,反而影响性能

ZGC流程

  • 指针着色技术(Color Pointers):他在指针中借了几个位出来做事情,所以它必须要求在64位的机器上才可以工作。并且因为要求64位的指针,也就不能支持压缩指针

    • ZGC中低42位表示使用中的堆空间

    • ZGC借几位高位来做GC相关的事情 (快速实现垃圾回收中的并发标记、转移和重定位等)

  • 根可达算法:来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的

    • 作为GC Roots的对象主要包括下面4种

      • 虚拟机栈(栈帧中的本地变量表):各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
      • 方法区中类静态变量:java类的引用类型静态变量。
      • 方法区中常量:比如:字符串常量池里的引用。
      • 本地方法栈中JNI指针:(即一般说的 Native方法)
  • 一次ZGC流程

      • 标记阶段 (标识垃圾)

        • 初始标记(有STW):从根集合(GC Roots)出发,找出根集合直接引用的活跃对象(根对象)
          • 初始标记只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,停顿时间不会随着堆的大小或者活跃对象的大小而增加
        • 并发标记(无STW):根据初始标记找到的根对象,使用深度优先遍历对象的成员变量进行标记
          • 扫描剩余的所有对象,这个处理时间比较长,所以走并发,业务线程与GC线程同时运行,但是这个阶段会产生漏标问题
        • 再标记(有STW):主要处理漏标对象,通过SATB算法解决(G1中的解决漏标的方案)
      • 转移阶段 (对象复制或移动):基于指针着色的并发转移算法

        • 并发转移准备(无STW ):分析最有价值GC分页
        • 初始转移(有STW):转移初始标记的存活对象同时做对象重定位
        • 并发转移(无STW):对转移并发标记的存活对象做转移(转发表)
          • 对象转移和插转发表做原子操作

      • 下次GC中的并发标记(同时做上次并发标记对象的重定位):基于指针着色的重定位算法

  • ZGC中读屏障:当应用线程从堆中读取对象引用时,就会执行这段代码(仅“从堆中读取对象引用”)

    • 涉及对象:并发转移但还没做对象重定位的对象(着色指针使用M0和M1可以区分)
    • 触发时机:在两次GC之间业务线程访问这样的对象
    • 触发操作:对象重定位+删除转发表记录(两个一起做原子操作)
  • ZGC中GC触发机制(JAVA16)

    • 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”
      • JVM启动预热,如果从来没有发生过GC,则在堆内存使用超过10%、20%、30%时,分别触发一次GC,以收集GC数据
    • 基于分配速率的自适应算法:最主要的GC触发方式(默认方式),其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”
      • 通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。日志中关键字是“Allocation Rate”
    • 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景
    • 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,如果我们的服务已经加了基于固定时间间隔的触发机制,可以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性
    • 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”
    • 外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”
    • 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”

ZGC参数设置

  • ZGC参数设置:ZGC 优势不仅在于其超低的 STW 停顿,也在于其参数的简单,绝大部分生产场景都可以自适应
    • 堆大小:Xmx。当分配速率过高,超过回收速率,造成堆内存不够时,会触发 Allocation Stall,这类 Stall 会减缓当前的用户线程。因此,当我们在 GC 日志中看到 Allocation Stall,通常可以认为堆空间偏小或者 concurrent gc threads 数偏小
    • GC 触发时机:ZAllocationSpikeTolerance, ZCollectionInterval
      • ZAllocationSpikeTolerance 用来估算当前的堆内存分配速率,在当前剩余的堆内存下,ZAllocationSpikeTolerance 越大,估算的达到 OOM 的时间越快,ZGC 就会更早地进行触发 GC
      • ZCollectionInterval 用来指定 GC 发生的间隔,以秒为单位触发 GC
    • GC 线程:ParallelGCThreads, ConcGCThreads
      • ParallelGCThreads 是设置 STW 任务的 GC 线程数目,默认为 CPU 个数的 60%
      • ConcGCThreads 是并发阶段 GC 线程的数目,默认为 CPU 个数的12.5%
      • 增加 GC 线程数目,可以加快 GC 完成任务,减少各个阶段的时间,但也会增加 CPU 的抢占开销,可根据生产情况调整
  • ZGC典型应用场景
    • 对于性能来说,不同的配置对性能的影响是不同的
      • 大堆场景下,ZGC 在各类 Benchmark 中能够超过 G1 大约 5% 到 20%
      • 在小堆情况下,则要低于 G1 大约 10%
    • 当前 ZGC 不支持压缩指针和分代 GC,其内存占用相对于 G1 来说要稍大,在小堆情况下较为明显,而在大堆情况下,这些多占用的内存则显得不那么突出
    • 以下两类应用强烈建议使用 ZGC 来提升业务体验
      • 超大堆应用。超大堆(百 G 以上)下,CMS 或者 G1 如果发生 Full GC,停顿会在分钟级别,可能会造成业务的中断,强烈推荐使用 ZGC
      • 当业务应用需要提供高服务级别协议(Service Level Agreement,SLA),例如 99.99% 的响应时间不能超过 100ms,此类应用无论堆大小,均推荐采用低停顿的 ZGC
  • ZGC生产注意事项
    • RSS 内存异常现象:ZGC 采用多映射 multi-mapping 的方法实现了三份虚拟内存指向同一份物理内存,而 Linux 统计进程 RSS 内存占用的算法是比较脆弱的,这种多映射的方式并没有考虑完整,因此根据当前 Linux 采用大页和小页时,其统计的开启 ZGC 的 Java 进程的内存表现是不同的
      • 在内核使用小页的 Linux 版本上, 这种三映射的同一块物理内存会被 linux 的 RSS 占用算法统计 3 次,因此通常可以看到使用 ZGC 的 Java 进程 的 RSS 内存膨胀了三倍左右,但是实际占用只有统计数据的三分之一,会对运维或者其他业务造成一定的困 扰
      • 在内核使用大页的 Linux 版本上,这部分三映射的物理内存则会统计到 hugetlbfs inode 上,而不是当 前 Java 进程上
    • 共享内存调整
      • ZGC 需要在 share memory 中建立一个内存文件来作为实际物理内存占用,因此当要使用的 Java 的堆大小大于 /dev/shm 的大小时,需要对 /dev/shm 的大小进行调整
        • 通常来说,命令如下(下面是将 /dev/shm 调整为 64G):
          • vi/etc/fstabtmpfs /dev/shm tmpfs defaults,size= 65536M00
        • 首先修改 fstab 中 shm 配置的大小,size 的值根据需求进行修改,然后再进行 shm 的 mount 和 umount
          • umount/dev/shmmount /dev/shm
    • mmap 节点上限调整
      • ZGC 的堆申请和传统的 GC 有所不同,需要占用的 memory mapping 数目更多,即每个 ZPage 需要 mmap 映射三次,这样系统中仅 Java Heap 所占用的 mmap 个数为 (Xmx / zpage_size) * 3,默认情况下 zpage_size 的大小为 2M
      • 为了给 JNI 等 native 模块中的 mmap 映射数目留出空间,内存映射的数目应该调整为 (Xmx / zpage_size) 3*1.2
      • 默认的系统 memory mapping 数目由文件 /proc/sys/vm/max_map_count 指定,通常数目为 65536,当给 JVM 配置一个很大的堆时,需要调整该文件的配置,使得其大于 (Xmx / zpage_size) 3*1.2
  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值