Java 服务老年代内存异常:大对象的影响与解决方案

最后修改于:

背景

某 Java 服务的老年代内存占用持续高位,怀疑可能存在内存泄漏的风险。 image1

排查记录

分析堆内存 dump

dump&&分析

  • jmap -dump:[live],format=b,file=<file-path> <pid>
    • live: if set, it only prints objects which have active references and discards the ones that are ready to be garbage collected. This parameter is optional.
    • format=b: specifies that the dump file will be in binary format. If not set, the result is the same.
    • file: the file where the dump will be written to
    • pid: id of the Java process

1、不进行 FullGC 直接 dump 内存

1
2
3
jmap -dump:format=b,file=dump.hprof 1

tar -cvzf dump.zip dump.hprof # 压缩,以方便下载

Leak Suspects 显示主要疑点全部与 com.ecwid.consul.v1.Response 有关。 image2 image3 image4

2、强制 FullGC 然后 dump 内存 依次执行以下命令,获取内存 dump 然后进行压缩(提高文件下载速度),然后通过服务治理平台-文件下载功能下载到本地,使用 eclipse mat 或者 VisualVM 进行分析。

1
2
3
jmap -dump:live,format=b,file=dump.hprof 1
 
tar -cvzf dump.zip dump.hprof

结论

  • Old Gen 虽然会达到很高占比,但总是能最终下降到低位,说明不存在严格意义上的内存泄露;
  • 根据 Leak Suspects 报告,Old Gen 占用高主要是类 com.ecwid.consul.v1.Response 导致,它代表从 consul 拉取的数据,包括两类;
    • CustomConfigWatch(javakit)获取的数据大小约 2MB;
    • ConsulCatalogWatch(SpringBoot)获取的数据大小约 170KB;
    • 这些数据会导致产生 Humongous 对象:比如网络层面接收这些数据时需要分配大的 byte 数组,转为 HttpResponse 时分配 char 数组(CharArrayBuffer);
  • 根据 Dominator Tree,除了 consul 数据,监控数据占内存同样很高 image5 image6

分析 GC 日志

结论

  • Old Gen 占比高并不是因为长生命周期对象晋升,而是大量分配 Humongous 对象(简称 H-Obj)—-因为根据监控,在没有 GC 时,Old Gen 的却一直在增长;
  • 每当进行 H-obj 分配时,就会触发 Mixed GC 的并发标记循环,进而导致一次 YGC(inital mark),回收死亡的 H-Obj;但如果当前处于 Mixed GC 阶段,则不会再触发一次;
  • 由于 H-obj 的分配频率非常高,因此实际会一直处于 Mixed GC 阶段(中间可以夹杂多次 YGC);
  • 在 YGC 以及 Mixed GC 的 cleanup 阶段,Old Gen 会大幅下降,因为此阶段会对 H-Obj 对象进行清理;
  • 虽然强制指定 -Xmn1g,但实际 Young 区还是会缩小;
  • dev & test 环境,在出现如下 GC 日志时,Old Gen 只有小幅下降:
1
2
3
4
5
6
    - 2023-06-30T07:10:32.869+0000: 3600.316: [GC pause (G1 Humongous Allocation) (mixed) 3600.316: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 26151, predicted base time: 42.80 ms, remaining time: 157.20 ms, target pause time: 200.00 ms]
    - 3600.316: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 384 regions, survivors: 0 regions, predicted young region time: 3.51 ms]
    - 3600.316: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: predicted time is too high, predicted time: 1.00 ms, remaining time: 0.60 ms, old: 153 regions, min: 83 regions]
    - 3600.316: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 384 regions, survivors: 0 regions, old: 153 regions, predicted pause time: 199.40 ms, target pause time: 200.00 ms]
    - G1 预测回收 Old Gen 时只剩余 0.6 ms,因此进行回收时只选取了少量的 Old Gen,且没有回收 Humongous 对象;
        - 根据 jdk8u  的源码,此预测似乎基于对先前若干次回收时间的统计;

整体结论

  • Old Gen 的快速上升主要是由于大量的 H-obj 分配,大对象的来源按频率排序,目前主要有 3 个:
    • ConsulCatalogWatch(SpringBoot)获取的数据大小约 170KB,约每 2 秒一次;
    • CustomConfigWatch(javakit)获取的数据大小约 2MB,约每 27 秒一次;
    • Prometheus&Metrics 监控数据,小于等于 1.3 MB,约 1 分钟一次??
  • H-obj 的分配速率与年轻代对象的增长速率大约成正比,且往往小于年轻代的增长速率;
  • 但存在一些特殊情况,在 mixed 回收阶段,由于 Old Gen 中可回收的 region 大于 5% 阈值,需要触发混合回收;且根据预测,可以留给 Old Gen 的回收时间非常短(不足 1ms),导致选择选择 CSet 时 Old Gen region 特别少,剩余 region 仍然大于 5%阈值,因此 mixed 阶段将持续,然后恶性循环(最多 8 次 mixed gc);中间如果出发 YGC,则 humongous 对象会被清除,Old Gen 占用会大幅下降(真正的老年代对象并不会清除);
    • 问题在于预测时间为什么会这么短?

优化方案

优化 PrometheusScrapeEndpoint

相关讨论:

思路?

  1. 使用一个 char[] 内存池,scrape 时从内存池获取一个足够大的 char 数组用于生成数据,完成后,将数组归还内存池?
  2. 减少 tag 数量? image7
Licensed under CC BY-NC-SA 4.0
最后更新于 2024/12/20 13:30 CST
本文总阅读量 次 本文总访客量 人 本站总访问量 次 本站总访客数
发表了20篇文章 · 总计32.36k字
本博客已稳定运行
使用 Hugo 构建
主题 StackJimmy 设计