JVM 老年代缓慢上涨,用 jmap 之外的三条排查线
老年代占用以每天几个百分点的速度往上走,Full GC 之后能降下来一点但降不干净,这种“缓慢上涨”最难查,因为一次 dump 往往看不出明显的大对象,堆里都是正常业务数据。
第一条线:看 GC 日志的增长斜率,而不是看单次 GC 的耗时。开启 -Xlog:gc* 后按时间把 Full GC 前后的老年代占用拉出来,如果每次 Full GC 后基线都在抬高,说明有对象被晋升后长期存活,重点查缓存、静态集合、线程池里的任务队列。如果基线不变只是间隔变短,方向是分配速率,属于参数问题。
第二条线:查 Metaspace 和类加载数量。很多人只盯堆,忽略了类加载器泄漏。用 jcmd VM.metaspace 和 VM.classloader_stats 看类和 Metaspace 的曲线,如果是热部署、动态代理、脚本引擎反复创建 ClassLoader,Metaspace 会持续增长,最终触发的是 Metaspace OOM 而不是堆 OOM。这类问题在堆 dump 里几乎看不出来。
第三条线:查直接内存和 mmap。nio 的 DirectByteBuffer 走的是堆外,堆 dump 里只有很小的 Cleaner 对象,真实占用在 Native Memory 里。用 jcmd VM.native_memory summary(需要启动时加 -XX:NativeMemoryTracking=summary)能看到 Internal 和 Other 段的变化;如果是文件映射导致的,检查 MappedByteBuffer 有没有及时 unmap,Linux 上可以用 pmap 对着进程看匿名映射的增长。
还有一个常被忽略的地方:容器环境里 JVM 看到的可用内存和 cgroup 限制不一致。老年代目标值按宿主机算的话,容器早就该 OOMKill 了,但表现却是堆缓慢上涨直到被杀死。用 -XX:MaxRAMPercentage 替代固定 -Xmx,并确认 UseContainerSupport 生效。
实践顺序建议:先拿一周的 GC 日志确认基线趋势,再决定要不要 dump;dump 之前先看 Metaspace 和 native memory,因为这两个方向不需要停机也不影响线上流量。盲目 dump 大堆,除了让服务卡顿几十秒,往往什么也证明不了。
- 点赞
- 收藏
- 关注作者
评论(0)