跳转至

GC监控

jmx_exporter

Q: 适用于什么类型的java项目?springboot和非springboot项目都能用这个exporter么?

A: 都可;基于 JMX(Java Management Extensions) 协议,是 JVM 层面的标准接口,与框架无关。

唯一区别:启动方式

  • Spring Boot 常作为 Java Agent 挂载(-javaagent:jmx_prometheus_javaagent.jar)
  • 非 Spring 项目若不方便挂 Agent,可使用 JMX to HTTP Bridge 或单独开 RMI 端口让 Prometheus 拉取。
基于K8s容器化的微服务项目如何部署

针对K8s容器化部署微服务项目,部署逻辑不变:每个Pod内挂载一个jmx_exporter Agent

  1. 制作基础镜像,将 jmx_prometheus_javaagent.jarconfig.yaml 提前打入业务基础镜像。
  2. 修改JVM启动参数,在Deployment的容器启动命令(ENTRYPOINT 或 args)中追加:-javaagent:/path/jmx_exporter.jar=8081:/path/config.yaml
  3. 暴露监控端口(Service + 注解),在Service中定义该端口,并给Pod打上注解,方便Prometheus自动发现。
  4. 资源限制,jmx_exporter本身轻量(约几十MB内存),但务必在容器的 resources.limits 中为这部分留足余量,防止OOM误杀业务进程。

重点监控的指标

压测时监控GC,核心目标:防范频繁GC导致“stop-the-world”(stw)停顿。

1.GC频次

当GC频次过于频繁需排查是否内存泄露或大对象分配。

Q: 频次多少才是频繁?

A: Young GC 每秒超过 5 次,或 Full GC 每小时超过 10 次。

2.停顿时间

  • 理想STW: Young GC 5至50ms;Full GC 200ms以下。否则通常会对高并发响应有明显影响。
  • 适用于一般Web服务,高敏感业务更严格。
Young GC 和 Full GC 的关系

Young GC 是清理年轻代(Eden+S0/S1),存活对象晋升到老年代。老年代满了会触发 Full GC(通常连带清理年轻代+老年代+元空间)。

  • Young GC 超 50ms,通常是因为年轻代过大(扫描复制耗时长)或存活对象过多。
  • Full GC 超 200ms,往往是老年代空间碎片化、大对象直接进老年代或内存泄漏。
优化方向

按优先级:

  1. 换垃圾回收器:高并发服务优先使用 G1GC(-XX:+UseG1GC),设定 -XX:MaxGCPauseMillis=200(让 JVM 自适应调节)。
  2. 调触发阈值:G1 默认老年代占比 45% 触发并发标记,若 Full GC 频繁,可调高至 -XX:InitiatingHeapOccupancyPercent=60,延迟 Full GC 触发点。
  3. 调大年轻代:若 Young GC 停顿时长超标,适当增大年轻代内存(-Xmn)减少晋升频率,但注意不要过大导致 Full GC 变重。
  4. 针对大对象:排查代码中一次性分配超大数据(如大 List/Map),G1 下可调 -XX:G1HeapRegionSize 避免大对象直接进老年代。

3.堆内存回收趋势

观察每次gc后存活内存,若持续阶梯上升,说明有内存泄漏风险。

伴随应用日志频繁抛出java.lang.OutOfMemoryError

OOM分类
  1. 内存泄漏:如死锁、资源未正确关闭
  2. 数据处理设计缺陷:内存溢出如一次性读数GB的文件至内存、递归深度失控
  3. 多进程/多线程的资源竞争
  4. JVM配置不当:如堆内存过小、元空间溢出等
  5. 其他

通过长时间压测,可以发现内存泄漏问题,报错OutOfMemory

4.内存利用率低

堆内存长期空闲超过 50%,但服务器内存充足(资源浪费)。

5.服务启动慢

应用启动耗时超过 5 分钟,可能是元空间分配不足,导致类加载效率低。

常见案例

1.rt周期性飙升

飙升时间点若跟 full gc 吻合,说明 stw 影响业务,需优化jvm参数(调大年轻代、调触发gc的内存阈值等)

2.CPU飙升且GC频繁

flowchart TD
    B[Step 1: dashboard<br/>看全局]
    B -->|GC次数增长快| D[Step 2: profiler<br/>抓分配热点]
    B -->|某线程CPU高| E[Step 3: thread -n 3<br/>捞高频线程]
    D --> F[生成火焰图<br/>定位new对象的方法]
    E --> G[查看线程堆栈<br/>找循环创建对象]
    F --> H[定位问题代码<br/>trace查方法内部调用耗时]
    G --> H
    H --> I{怀疑内存泄漏?}
    I -->|是| J[Step 4: heapdump<br/>查存活对象]
    I -->|否| K[修复代码]
    J --> K
Use mouse to pan and zoom

Step 1:dashboard

dashboard

  • gc.ps_scavenge.count: Young GC 次数
  • gc.ps_scavenge.time(ms): Young GC 总耗时
  • gc.ps_marksweep.count: Full GC 次数
  • gc.ps_marksweep.time(ms): Full GC 总耗时

以上GC指标也可通过jvm命令直接看汇总:GARBAGE-COLLECTORS区域,显示 PS Scavenge 和 PS MarkSweep 的总次数和总耗时。

结合上文所述指标

  • GC频繁: Young GC 每秒超过 5 次,或 Full GC 每小时超过 10 次。
  • GC停顿时间: Young GC 超 50ms,或 Full GC 超 200ms。

Step 2:profiler

# 开启内存分配采样
profiler start --event alloc

# 运行1-2分钟,捕获对象分配热点
# 停止采样,生成火焰图 HTML
profiler stop
  1. 看火焰图,纵向深度为调用栈,横向宽度为cpu耗时占比,优先找图中最宽的平台。
  2. 使用trace命令进一步挖掘怀疑方法的内部调用耗时。
# 只追踪耗时 >10ms 的调用
trace com.example.UserService processOrder '#cost > 10'

Step 3:thread

  1. thread -n 3,打印cpu占用前3的线程
  2. 使用trace命令进一步挖掘怀疑方法的内部调用耗时。

Step 4:heapdump

heapdump --live /tmp/dump.hprof

dump live 对象到指定文件,展示存活对象实例数 Top N

heapdump的风险

  • heapdump执行时通常会强制Full GC,如果堆内存很大,可能会导致应用线程完全卡死几分钟甚至更久,影响原本正在恢复的业务。
  • heapdump文件大小基本等于堆内存大小,需关注磁盘IO与空间。

建议在heapdump前,先histogram看对象分布,只统计JVM里对象的实例数量和占用字节数,几乎不影响业务。

  • 如果发现 byte[] 或某个业务大对象实例数异常多,怀疑内存泄漏。
  • 如果发现对象总数正常,只是特定大集合占着,怀疑死锁。

搭配Eclipse MAT开源工具分析

  1. Leak Suspects Report(泄漏嫌疑报告):定位内存泄漏,自动分析并给出内存占用最大的几个问题,点击 Details 可以查看详细的引用链,快速定位到问题代码。
  2. Histogram(直方图):查找大对象,快速找出占用内存最多的类型。
  3. Dominator Tree(支配树):快速找到占用内存最大的具体对象实例。
  4. Thread Overview 视图:分析线程,查看每个线程的栈信息和局部变量,分析是否有线程持有大量对象引用。

关键术语解释

  • Shallow Heap(浅堆):对象本身占用的内存大小。
  • Retained Heap(深堆/保留堆):当该对象被回收时,能够释放出的全部内存大小。也是分析内存泄漏的核心指标。

参考资料:

JVM 调参实战指南