JVM垃圾回收(GC)是Java应用内存管理的核心环节,观察GC日志生成的堆内存使用曲线(如JVisualVM、GC日志绘图工具等)能直观反映JVM状态。不同的图形形态对应不同的内存状态和问题。本文通过分析几种典型的GC图形,帮你判断JVM当前状况,结合代码示例和优化方案,让你更好定位和解决问题。

1. 正常GC(锯齿状)

图片.png

  • 图形表现:堆内存呈规律锯齿状上升后下降。
  • 是否异常:无异常,健康状态。
  • 代码实例:

      @PostMapping("/getById")
      public Result<DemoVO> getById(@Valid @RequestBody GetDemoByIdParam param) {
          DemoVO demoVO = demoManager.getById(param.getId());
          return Result.ofSuccess(demoVO);
      }
  • 说明:每次内存到达阈值后都会触发Minor GC,及时回收。
  • 解决方式:不需要解决,这是预期行为。

2. 缓存未清理(刀锯片状)

图片.png

  • 图形表现:内存使用量波动很小,锯齿变钝,变小,长时间不能下降
  • 是否异常:异常,缓存未设置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);
      }
    1. 说明:缓存增长不受控,占满整个堆或者老年代。
  • 解决方式:

    • 使用Caffine、Guava Cache设置TTL或者最大容量。
    • 定期清理或者使用LRU、LFU
    • 配置JVM参数观察 -Xmx是否太小

3. 内存泄漏(向上阶梯状)

图片.png

  • 图形表现:堆内存持续上涨,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(断崖式上升)

    图片.png

  • 图形表现:堆频繁达到高点并触发 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)

图片.png

  • 图形表现:堆使用不高,但频繁 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)。

苏凌峰
76 声望42 粉丝

你的迷惑在于想得太多而书读的太少。