在 4GB 内存的 Ubuntu 系统上,Java 项目出现内存持续增长(Memory Leak 或配置不当)是典型问题。以下是系统化的排查与调优步骤:
一、确认现象
-
观察内存趋势
free -h # 查看整体内存使用 top # 实时查看 Java 进程内存占用(按 P 排序) ps aux --sort=-%mem | grep java若
java进程的RES(常驻内存)持续上升且未释放,基本可判定为内存泄漏或堆配置过大。 -
区分堆内存 vs 非堆内存
- 使用
jstat -gc <pid> 1000每秒输出 GC 统计,关注S0,S1,Eden,Survivor,Old区域增长是否收敛。 - 若 Old Gen 持续增长且 Full GC 频繁但无法回收 → 疑似内存泄漏。
- 使用
二、定位根因
1. 生成并分析 Heap Dump
# 确保已安装 jdk-tools(含 jmap/jcmd)
sudo apt install openjdk-17-jdk-headless # 根据实际版本调整
# 方式一:手动触发 dump(推荐)
jmap -dump:live,format=b,file=heap.hprof <pid>
# 方式二:通过 JMX(需启动时加参数)
java -Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-jar your-app.jar
# 然后用 VisualVM / JConsole 连接 9999 端口
⚠️ 注意:生产环境 dump 前务必评估磁盘空间(heap.hprof 可能达数 GB),建议先限流或低峰期操作。
2. 分析 Heap Dump
-
工具选择:
- Eclipse MAT(免费、强大,支持 OQL 查询)
- JProfiler(商业,可视化好)
jhat(轻量,浏览器访问http://localhost:7000)
-
关键检查点:
Leak Suspects Report(MAT 自动生成)- 哪些对象持有大量引用?(如静态集合、ThreadLocal、缓存未清理)
- 是否存在
byte[]数组异常增长? - 第三方库(如 Spring Cache、Guava Cache)是否配置了无上限缓存?
3. 检查非堆内存
若堆正常但总内存仍增长:
- 检查 Native Memory Tracking(NMT):
java -XX:NativeMemoryTracking=summary -jar your-app.jar # 运行一段时间后执行: jcmd <pid> VM.native_memory summary常见原因:DirectByteBuffer 泄漏、JNI 资源未释放、线程栈过大。
三、针对性调优策略
✅ 立即生效的 JVM 参数优化(适配 4GB 系统)
-Xms512m -Xmx1g # 初始/最大堆设为 1G(留 2G+ 给 OS + 其他服务)
-XX:+UseG1GC # G1 垃圾回收器更适合中小堆
-XX:MaxGCPauseMillis=200 # 控制停顿时间
-XX:InitiatingHeapOccupancyPercent=45 # G1 提前触发并发标记
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m # 限制元空间
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/java/
📌 原则:总堆 ≤ 物理内存 × 60%(Ubuntu 还需预留 OS + Docker + 其他服务)。
✅ 应用层修复建议
| 问题类型 | 解决方案 |
|---|---|
| 静态集合无限增长 | 改用 WeakHashMap 或定期清理;避免 static List<...> 累积数据 |
| 缓存无界 | 设置 TTL/TTL + Max Size(如 Caffeine:maximumSize(1000), expireAfterWrite(10m)) |
| ThreadLocal 未 remove | 在 finally 块中调用 threadLocal.remove() |
| 大对象频繁创建 | 复用对象池(如 HikariCP 连接池、对象池库) |
| 第三方库默认行为 | 检查日志框架、HTTP Client、数据库驱动等默认配置 |
✅ 系统级防护
# 限制 Java 进程可用内存(cgroup v2,Ubuntu 20.04+)
sudo mkdir -p /sys/fs/cgroup/java-limit
echo "memory.max=1.5G" | sudo tee /sys/fs/cgroup/java-limit/memory.max
echo $$ | sudo tee /sys/fs/cgroup/java-limit/cgroup.procs
或使用 systemd 限制(推荐):
# /etc/systemd/system/myapp.service.d/override.conf
[Service]
MemoryMax=1.5G
MemoryHigh=1.2G
四、验证效果
- 重启应用后监控 24 小时:
watch -n 60 'jstat -gc <pid> && echo "---"' - 预期结果:
- Old Gen 稳定在设定值内波动
- Full GC 间隔合理(如 >30min)
- 总 RSS 不再线性增长
附:快速诊断脚本(保存为 check-java.sh)
#!/bin/bash
PID=$(pgrep -f "your-app-name")
if [ -z "$PID" ]; then echo "No Java process found"; exit 1; fi
echo "=== Process Info ==="
ps -o pid,rss,vsz,comm -p $PID
echo -e "n=== GC Stats (last 5s) ==="
jstat -gc $PID 1000 5
echo -e "n=== Top Objects (via jcmd) ==="
jcmd $PID GC.class_histogram | head -20
💡 提示:优先从代码逻辑入手修复泄漏,JVM 调优只是缓解手段。若短期无法上线热修复,可考虑滚动发布 + 灰度验证。
需要我帮你分析具体的 heap dump 片段或提供某类泄漏(如 Spring Cache、Netty DirectBuffer)的典型修复代码模板吗?
CLOUD技术笔记