JVM垃圾回收(GC)是Java应用内存管理的核心环节,观察GC日志生成的堆内存使用曲线(如JVisualVM、GC日志绘图工具等)能直观反映JVM状态。不同的图形形态对应不同的内存状态和问题。本文通过分析几种典型的GC图形,帮你判断JVM当前状况,结合代码示例和优化方案,让你更好定位和解决问题。
1. 正常GC(锯齿状)
- 图形表现:堆内存呈规律锯齿状上升后下降。
- 是否异常:无异常,健康状态。
代码实例:
@PostMapping("/getById") public Result<DemoVO> getById(@Valid @RequestBody GetDemoByIdParam param) { DemoVO demoVO = demoManager.getById(param.getId()); return Result.ofSuccess(demoVO); }
- 说明:每次内存到达阈值后都会触发Minor GC,及时回收。
- 解决方式:不需要解决,这是预期行为。
2. 缓存未清理(刀锯片状)
- 图形表现:内存使用量波动很小,锯齿变钝,变小,长时间不能下降
- 是否异常:异常,缓存未设置TTL或者最大容量限制
代码示例:
private Map<Long,DemoVO> cache = new HashMap<>(); @PostMapping("/getById") public Result<DemoVO> getById(@Valid @RequestBody GetDemoByIdParam param) { DemoVO demoVO = demoManager.getById(param.getId()); cache.put(param.getId(),demoVO); return Result.ofSuccess(demoVO); }
- 说明:缓存增长不受控,占满整个堆或者老年代。
解决方式:
- 使用Caffine、Guava Cache设置TTL或者最大容量。
- 定期清理或者使用LRU、LFU
- 配置JVM参数观察 -Xmx是否太小
3. 内存泄漏(向上阶梯状)
- 图形表现:堆内存持续上涨,GC后没有释放,形成“台阶”。
- 是否异常:异常,引用未释放,导致对象无法回收
代码示例:
public class UserContextHolder { private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>(); public static void setUser(User user) { userThreadLocal.set(user); } public static User getUser() { return userThreadLocal.get(); } public static void clear() { userThreadLocal.remove(); // 如果不调用这行,Tomcat线程复用将泄漏 } }
解决方式:
- 使用 InheritableThreadLocal / ThreadLocal.withInitial + 自动清理框架
- 使用 ThreadLocal注意使用完毕后清理掉
- 注意及时清理静态集合持有对象引用
4. 频繁Full GC(断崖式上升)
- 图形表现:堆频繁达到高点并触发 Full GC,GC时间长,应用卡顿,且内存占用无法回收。
- 是否异常:异常,内存占用过大导致不足。
代码示例:
@PostMapping("/getById") public Result<DemoVO> getById(@Valid @RequestBody GetDemoByIdParam param) { //一次性查出非常多的List数据,导致OOM List<DemoVO> demoVO = demoManager.getById(param.getId()); return Result.ofSuccess(demoVO); }
- 说明:大量对象撑爆内存
解决方式:
- 降低对象创建的大小,不要一次查太多数据
- 增加 -Xmx、-XX:NewRatio
- 防止僵尸进程,也可以加上-XX:OnOutOfMemoryError="kill -9 %p",报错的时候就停止自己的进程,然后配合K8s或者服务重启监控程序让服务再次重启。
5. 元空间OOM(占用低频繁GC)
- 图形表现:堆使用不高,但频繁 GC 或抛出 java.lang.OutOfMemoryError: Metaspace。
- 是否异常:异常,动态类加载太多,类卸载不及时。
代码示例:
List<Class<?>> classes = new ArrayList<>(); ClassPool classPool = ClassPool.getDefault(); int count = 0; while (true) { CtClass ctClass = classPool.makeClass("com.example.Generated" + count++); classes.add(ctClass.toClass()); // 加载类并放入元空间 System.out.println("Loaded class count: " + count); }
- 备注:也可以看我这篇博客:MyBatis-Plus的Lambda表达式引发Metaspace OOM深度分析与解决方案
- 说明:每次都加载新类,导致元空间不断增长。
解决方式:
- 避免频繁动态加载类
- 增加元空间上限:-XX:MaxMetaspaceSize
- 检查是否有框架频繁创建代理类(如 CGLIB、Javassist,Mybatis-Plus)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。