Ubuntu上Java日志显示资源占用过高的定位与解决
一、先快速定位资源瓶颈
- 用系统工具确认瓶颈类型
- CPU:top/htop 观察进程CPU占用;按 1 展开多核;用 pidstat -u -p 1 查看线程级CPU。
- 内存:top/VmRSS;pmap -x | tail;smem -P java 看 USS/PSS;若 RSS 明显高于 -Xmx,可能存在堆外内存或本地库占用。
- 文件句柄:lsof -p | wc -l;/proc//limits 查看 ulimit。
- 线程数:ps -eLf | grep | wc -l;jstack | grep “java.lang.Thread.State” | sort | uniq -c。
- 磁盘与网络:iostat -x 1;iftop/nload;df -h;du -sh /var/log/。
- 用JDK自带工具看JVM内部
- 实时:jstat -gc -t 1s(关注 YGC/YGCT、FGC/FGCT、GCT 的增长趋势)。
- 内存概要:jmap -heap ;必要时 jmap -histo:live 观察对象数量与占用。
- 线程与阻塞:jstack > jstack.txt;配合 top -Hp 将高CPU线程的十进制转十六进制后在 jstack 中定位。
- 堆转储:jmap -dump:live,format=b,file=heap.hprof (仅在必要时执行,避免业务停顿)。
- GC日志与暂停
- 启动时加上 -Xlog:gc,gc+heap=debug:file=/var/log/app-gc.log:time,tags*(JDK 9+),或用 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/app-gc.log(JDK 8)。
- 关注 Full GC 次数/耗时、晋升失败(promotion failure)、元空间(Metaspace)是否持续增长。
- 堆外与本地内存
- 开启 -XX:NativeMemoryTracking=detail,用 jcmd VM.native_memory detail 查看分类占用;若 RSS 明显高于堆上限,优先排查 DirectByteBuffer、JNI/Native 库、第三方本地组件(如 RocksDB、Netty 的堆外缓冲)。
二、常见根因与对应处置
- 堆内存不足或内存泄漏
- 现象:FGC 频繁、Old 区回收后仍增长,最终出现 OutOfMemoryError: Java heap space。
- 处置:
- 先做堆转储并用 Eclipse MAT 或 VisualVM 分析“dominator tree”“shortest path to GC roots”,定位被静态集合、缓存、监听器等长期持有的对象;修复泄漏或引入弱/软引用与过期淘汰策略。
- 合理设置 -Xms/-Xmx(建议等值,避免运行期扩堆抖动),并结合业务峰值与对象生命周期调优新生代/老年代比例。
- 堆外内存与本地库
- 现象:top 的 RSS 持续高于 -Xmx,NMT 显示 Internal/Thread/Code 等分类增长明显。
- 处置:
- 核查 DirectByteBuffer 使用与释放路径(如 Netty ByteBuf 池化配置);必要时用 -XX:MaxDirectMemorySize 设限验证。
- JNI/Native:用 jemalloc/gperftools 采样/剖析本地分配栈;检查 RocksDB、压缩库、解析器等是否频繁创建未复用或泄漏。
- GC策略不匹配导致停顿过长
- 现象:日志中单次 GC 暂停很长或停顿抖动大,影响 RT。
- 处置:
- JDK 8 可评估 G1 或 Parallel Old;JDK 11+ 优先 ZGC(极低停顿、大堆友好)。
- 结合负载特征设置停顿目标与区域大小,减少晋升压力与并发标记压力。
- 线程、连接与文件句柄泄漏
- 现象:线程数/句柄数随时间增长,jstack 看到大量 RUNNABLE/WAITING 线程,日志出现 “too many open files”。
- 处置:
- 修正线程池/连接池配置(核心线程、队列、超时、回收策略),确保 close()/release() 在 finally 或 try-with-resources 中执行;
- 调整 ulimit -n,并检查日志框架、HTTP 客户端、数据库驱动的 I/O 与连接泄漏。
- 日志自身造成的放大效应
- 现象:应用频繁拼接/打印大对象或堆栈,磁盘 I/O 与锁竞争导致 CPU/IO 飙升。
- 处置:降低 DEBUG/TRACE 级别;异步/批量刷盘;精简日志模板;避免打印大对象与全量堆栈;使用结构化日志与采样。
三、可落地的优化与配置示例
- 堆与GC基础
- 建议将 -Xms 与 -Xmx 设为相同值(如 -Xms4g -Xmx4g),减少扩缩堆带来的抖动;结合对象生命周期设置新生代大小(如 -Xmn2g 或 -XX:NewRatio=2)。
- JDK 8:可用 -XX:+UseG1GC;JDK 11+:优先 -XX:+UseZGC(若需兼顾吞吐与低延迟)。
- GC日志与监控
- JDK 9+:
- -Xlog:gc*,gc+heap=debug:file=/var/log/app-gc.log:time,tags
- -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heapdump.hprof
- JDK 8:
- -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/app-gc.log
- -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heapdump.hprof
- 堆外与本地内存
- 开启 -XX:NativeMemoryTracking=detail 做基线对比;对 DirectByteBuffer 使用池化并显式释放;JNI 场景接入 jemalloc/gperftools 定位热点分配路径。
- 线程与连接治理
- 统一使用带上限与回收策略的线程池/连接池;对外资源(流、通道、语句、会话)在 finally/try-with-resources 中关闭;监控并告警线程数与句柄数异常增长。
- 日志侧优化
- 降低冗余日志级别;避免打印大对象与频繁堆栈;采用异步日志与合理的滚动策略(如基于时间/大小)。
四、最小化复现与持续治理
- 复现与压测
- 在预发/灰度环境用真实流量或回放流量压测,开启 GC 日志与必要的 NMT,观察 GC 暂停、晋升失败、堆外增长 等指标拐点。
- 线上诊断与热修复
- 不重启定位 CPU/内存热点:用 Arthas 的 profiler 生成火焰图、watch/trace 观察方法耗时与入参出参;必要时 jstack/线程dump 结合分析。
- 建立基线
- 固化“GC 暂停 P95/P99、Old 区使用率、线程数、句柄数、RSS”等指标的常态与告警阈值;每次发布前后对比,异常即回滚与排查。
- 代码与架构治理
- 对缓存/会话/监听器等“长生命周期容器”引入过期与淘汰;对大对象采用分批/流式处理;对外部 I/O 严格释放与超时控制。