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。
- 制作基础镜像,将
jmx_prometheus_javaagent.jar和config.yaml提前打入业务基础镜像。 - 修改JVM启动参数,在Deployment的容器启动命令(ENTRYPOINT 或 args)中追加:
-javaagent:/path/jmx_exporter.jar=8081:/path/config.yaml - 暴露监控端口(Service + 注解),在Service中定义该端口,并给Pod打上注解,方便Prometheus自动发现。
- 资源限制,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,往往是老年代空间碎片化、大对象直接进老年代或内存泄漏。
优化方向
按优先级:
- 换垃圾回收器:高并发服务优先使用 G1GC(-XX:+UseG1GC),设定 -XX:MaxGCPauseMillis=200(让 JVM 自适应调节)。
- 调触发阈值:G1 默认老年代占比 45% 触发并发标记,若 Full GC 频繁,可调高至
-XX:InitiatingHeapOccupancyPercent=60,延迟 Full GC 触发点。 - 调大年轻代:若 Young GC 停顿时长超标,适当增大年轻代内存(-Xmn)减少晋升频率,但注意不要过大导致 Full GC 变重。
- 针对大对象:排查代码中一次性分配超大数据(如大 List/Map),G1 下可调 -XX:G1HeapRegionSize 避免大对象直接进老年代。
3.堆内存回收趋势
观察每次gc后存活内存,若持续阶梯上升,说明有内存泄漏风险。
伴随应用日志频繁抛出java.lang.OutOfMemoryError
OOM分类
- 内存泄漏:如死锁、资源未正确关闭
- 数据处理设计缺陷:内存溢出如一次性读数GB的文件至内存、递归深度失控
- 多进程/多线程的资源竞争
- JVM配置不当:如堆内存过小、元空间溢出等
- 其他
通过长时间压测,可以发现内存泄漏问题,报错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
Step 1: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
- 看火焰图,纵向深度为调用栈,横向宽度为cpu耗时占比,优先找图中最宽的平台。
- 使用
trace命令进一步挖掘怀疑方法的内部调用耗时。
# 只追踪耗时 >10ms 的调用
trace com.example.UserService processOrder '#cost > 10'
Step 3:thread
thread -n 3,打印cpu占用前3的线程- 使用
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开源工具分析
- Leak Suspects Report(泄漏嫌疑报告):定位内存泄漏,自动分析并给出内存占用最大的几个问题,点击 Details 可以查看详细的引用链,快速定位到问题代码。
- Histogram(直方图):查找大对象,快速找出占用内存最多的类型。
- Dominator Tree(支配树):快速找到占用内存最大的具体对象实例。
- Thread Overview 视图:分析线程,查看每个线程的栈信息和局部变量,分析是否有线程持有大量对象引用。
关键术语解释
- Shallow Heap(浅堆):对象本身占用的内存大小。
- Retained Heap(深堆/保留堆):当该对象被回收时,能够释放出的全部内存大小。也是分析内存泄漏的核心指标。
参考资料:
