掘金 后端 ( ) • 2024-06-16 20:46

theme: awesome-green highlight: a11y-dark

👈👈👈 欢迎点赞收藏关注哟

首先分享之前的所有文章 >>>> 😜😜😜
文章合集 : 🎁 https://juejin.cn/post/6941642435189538824
Github : 👉 https://github.com/black-ant
CASE 备份 : 👉 https://gitee.com/antblack/case

一. 前言

上一篇 # 深刻体验不同对象下 G1 回收器的内存变化 , 了解完了对象对垃圾回收的影响 ,这一篇来看一下 JVM 参数对垃圾回收的影响

二. 常见的参数汇总

image.png

image.png

三. 我们有哪些可用的垃圾回收

这一块也是老生常谈的点了, 就不详细来说了, 只是列出来, 便于后文的理解 :

以下是包括 ParNew、Parallel Scavenge 和 Parallel Old 在内的完整表格:

收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景 Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式 Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案 ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合 Parallel Scavenge 并行 新生代 复制算法 最大化吞吐量 在后台运算而不需要太多交互的任务 Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务 CMS 并发 老年代 标记-清除 降低暂停时间 集中在互联网站或B/S系统服务端上的Java应用 G1 并发 新生代 / 老年代 标记-整理+复制算法 可预测的暂停时间控制 面向服务端应用,将来替换CMS Shenandoah 并发 整个堆 标记-复制-压缩 极低的暂停时间 适合大内存低延迟应用 ZGC 并发 整个堆 标记-复制-重定位 极低的暂停时间 适合大内存低延迟应用

我这里主要以 G1 为例 , 其中会涉及到一些通用的属性

四. 初始流程

在上一篇中我们尽量模拟实际的场景,基于软引用模拟可被回收的对象,同时基于一定的几率模拟始终不会被回收的对象。

初始参数

-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-Xms512m
-Xmx2048m

验证代码

private void add() {
    // 创建一个存储软引用的列表
    List<SoftReference<byte[]>> softReferenceList = new ArrayList<>();
    List<WeakReference<byte[]>> weakReferenceList = new ArrayList<>();
    ArrayList<byte[]> retainedObjects = new ArrayList<>();
    Random random = new Random();

    // 分配内存并使用软引用引用这些内存块
    try {
        while (true) {
            int i = atomicInteger.addAndGet(1);
            if (i % 10 < 1) {
                // 弱引用每次 GC 都会被回收
                byte[] largeArray = buildArray(); // 64K - 0.64M
                WeakReference<byte[]> weakReference = new WeakReference<>(largeArray);
                weakReferenceList.add(weakReference);
            } else if (i % 10 < 3) {
                //以一定概率保留对象引用,防止它们被回收
                byte[] largeArray = buildArray();
                if (random.nextInt(10) < 5) {
                    retainedObjects.add(largeArray);
                }
                // 模拟被回收的常见
                if (random.nextInt(1000) < 10) {
                    retainedObjects = new ArrayList<>();
                }
            } else {
                // 软引用内存不足时被回收
                SoftReference<byte[]> softReference = new SoftReference<>(buildArray());
                softReferenceList.add(softReference);
            }
        }
    } catch (Exception e) {
    }
}

基于这个模型,我们可以跑出一个初版数据 :

  • 由于有无法清除的对象 ,老年代最小占用量在逐步上升
  • 由于年轻代远远没有达到年轻代的阈值 ,所以年轻代每次都是被 FullGC 回收走了
    • 这里可以看左下角那张图的黄线部分 , 后半段基本上没有了 Minor GC
  • 除了 S1 区域的老问题 ,基本上符合常见的场景

image.png

五. 进行配置调整

  • 为了更快的验证 ,下面的所有操作都会加快对象产生的速度

5.1 通用配置

在初始化一个新应用的时候 ,有些配置是需要我们默认要加上的 :

  • 设置内存的各项作用指标 , 基于服务器的配置进行设计
    • -Xms512m -Xmx2048m -Xmn512m
  • 选择对应的垃圾回收器 :既可以选择单一的 ,也可以进行组合使用
    • -XX:+UseParallelGC -XX:+UseParallelOldGC
    • -XX:+UseG1GC
  • 除此之外还有一些日志的配置,这些平时一般不配,配了就是有问题了
    • -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
    • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps

在这里我们垃圾回收器选择使用 G1 , 不使用其他的回收器, 以下是核心的测试流程 :

5.2 设置最大晋升年龄阈值

// - 通过10次垃圾回收才会来到老年代
-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-Xms512m
-Xmx2048m
-XX:MaxTenuringThreshold=10
  • 预期 : 年轻代占用的空间应该明显变多 ,S1 区域会有比较大的增加 ,且一直保持活跃
  • 结果 : 基本符合预期
      1. Survivor 区间并没有像之前一样缩减到最小值后不再变化
      1. 年轻代变化较小 ,主要是 Eden 区给的过大 ,不容易触发 YoungGC

image.png

5.3 设置年轻代的可用大小

// - 通过10次垃圾回收才会来到老年代
-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-Xms512m
-Xmx2048m
-XX:MaxTenuringThreshold=10
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=20
  • 备注 : 这里很奇怪 ,按理说我的 JDK 应该没有这个参数的 ,但是确实是生效了
  • 预期 : 我们设置了G1年轻代最大为20% (等同于 Xmn), 那么年轻代最大应该为 409M
  • 结果 : 基本符合预期
    • 相比之前 Eden 区动不到动到 700 M ,修改后的内存要小得多 (图一
    • 由于年轻代分配的空间更小了 ,年轻代垃圾回收的频率明显变多了 (图三
    • 可以看出 ,不同的场景下 ,年轻代合理配置能让整个回收更加健康 (图二
  • 问题可以看出来 ,申请的空间和使用的空间差距有点大

image.png

image.png

image.png

如果把这个参数调大后

-XX:G1NewSizePercent=30
-XX:G1MaxNewSizePercent=50
  • 可以明确看到 ,确实生效了

image.png

5.4 配置停顿时间

❗❗❗特别注意 ,停顿时间是个很玄学的概念 ,没有特殊原因 ,不要设置它。(官方建议)

// - 为了凸显效果 ,我们把这个值设置得离谱点
-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-Xms512m
-Xmx2048m
-XX:MaxTenuringThreshold=10
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=20

// 会尝试两种修改的方式, 一种是往大了离谱点 , 一种是往小了改
-XX:MaxGCPauseMillis=1000
-XX:MaxGCPauseMillis=3
  • 预期 : 设置后我也猜不到预期,直接看结果吧
  • 结果 : 完全不符合预期
    • 当将这个值调的非常离谱的时候 ,其实是不会生效的
    • 在 JVM 里面 ,MaxGCPauseMillis 是预期目标 ,这是一个软目标,JVM 将尽最大努力实现它
    • 既然是预期值 ,也就意味着可能无法达到这个目标 ,也就没有太明显的作用

设置一个较低值的效果

  • 虽然没有明确控制数值在某个区间内 ,但是能看到对比没有设置 ,还是由很大区别的

image.png

5.5 设置垃圾回收的间隔

如果并发不太高 ,我不期望回收得特别频繁,可以通过垃圾回收间隔来实现 :

-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-Xms512m
-Xmx2048m
-XX:MaxGCPauseMillis=200
// 这里由 500 改成 2000 ,来看一下效果
-XX:GCPauseIntervalMillis=2000

image.png

  • 预期 : 当修改垃圾回收间隔后 ,回收得频率应该要变低
  • 结果 : 符合预取 ,回收频率明显降了很多 ,
    • 主要的差别可以从左下角那张图看到 ,回收的频率由 1次/秒 变成了 0.25次/秒
    • 相当于 4 秒才发生一次 ,和我们预期的 4 倍相当

5.6 设置老年代回收阈值

我们知道老年代内存不够时就会触发垃圾回收 ,对于一些场景需要修改这个触发时机则可以通过 InitiatingHeapOccupancyPercent :

-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-Xms512m
-Xmx2048m
-XX:MaxTenuringThreshold=10
// 此处由 30 -> 70
-XX:InitiatingHeapOccupancyPercent=30
  • 期望 : 首次触发垃圾回收的时机变动,符合 30% 和 70% 的现象
  • 实际 : 几乎无效 ,后面查询各项资料后发现 ,JDK8 早期版本存在很多问题 ,对于这个配置没有很好的兼容
    • 首次 major GC 实际上还是在老年代无法申请到内存时触发
    • 考虑 JDK 版本问题 ,尝试了 JDK11 后再次验证

image.png

JDK 11 结果复现

  • 结果 :可以看到 ,第一次 major GC 确实是在 30% 左右进行触发

image.png

  • 那么把这个值进行调整 ,设置为 70% 后的表现如何 ?
    • 这里要注意 ,这个到底是针对总的堆内存的 70% 还是 老年代的 70% 我是不太确定的,不同版本不一样
    • 可以参考这篇文章 ,时间有限 ,我就没测试了 @ https://heapdump.cn/article/2712390
    • 70% 的效果不明显和 JDK的处理机制有关 ,动态的处理方式并不能保证完全和数值一致

需要注意 : 如果一开始内存就超过这个值了 ,那么其实设置也没有很好的效果

  • 这里内存在启动的时候就快速跑到了 50% 以上 ,也不存在触发 FullGC 的阈值了

image.png

六. 不同版本对垃圾回收的影响

image.png

  • 基于简单看到 ,相同的代码 ,相同的运行环节下面 ,JDK 8 和 JDK 11 使用 G1 的差距很大
  • 甚至于在同一个大版本下 ,不同 JDK8 的小版本里面对于 Region 的各项数据计算都会有不同的差异

总结

  • 不同的参数 一定要 注意 JDK 版本号 ,版本不一样对应的实现和方案都不一样
  • 虽然采用一样的环境和代码 ,但是这种不可能完全体现出配置的作用,毕竟没读过源码,里面有些机制还不清楚
  • 修改配置要谨慎 , 首先是可能会失效 , 同时不同版本的效果又不一样 ❗❗❗
  • 关于核心配置每经过压测和测试环境测试别直接上生产, 尤其是早期的 JDK 版本 ,可能有 BUG

附录 : 如何查看支持的 JVM 参数及默认参数

这是我这个 JDK 版本支持的参数 ,这里列出来了

@ https://www.oracle.com/java/technologies/javase-documentation.html

> java -XX:+PrintFlagsFinal
   double G1ConcMarkStepDurationMillis             = 10.000000                                 {product} {default}
     uint G1ConcRefinementThreads                  = 23                                        {product} {ergonomic}
    uintx G1ConfidencePercent                      = 50                                        {product} {default}
   size_t G1HeapRegionSize                         = 4194304                                   {product} {ergonomic}
    uintx G1HeapWastePercent                       = 5                                         {product} {default}
    uintx G1MixedGCCountTarget                     = 8                                         {product} {default}
    uintx G1PeriodicGCInterval                     = 0                                      {manageable} {default}
     bool G1PeriodicGCInvokesConcurrent            = true                                      {product} {default}
   double G1PeriodicGCSystemLoadThreshold          = 0.000000                               {manageable} {default}
     intx G1RSetUpdatingPauseTimePercent           = 10                                        {product} {default}
     uint G1RefProcDrainInterval                   = 1000                                      {product} {default}
    uintx G1ReservePercent                         = 10                                        {product} {default}
    uintx G1SATBBufferEnqueueingThresholdPercent   = 60                                        {product} {default}
   size_t G1SATBBufferSize                         = 1024                                      {product} {default}
   size_t G1UpdateBufferSize                       = 256                                       {product} {default}
     bool G1UseAdaptiveIHOP                        = true                                      {product} {default}
    uintx MaxGCMinorPauseMillis                    = 18446744073709551615                      {product} {default}
    uintx MaxGCPauseMillis                         = 200                                       {product} {default}
      int ParGCArrayScanChunk                      = 50                                        {product} {default}
    uintx ParallelGCBufferWastePct                 = 10                                        {product} {default}
     uint ParallelGCThreads                        = 23                                        {product} {default}


// .................