一次线上系统OutOfMemoryError问题定位及原因分析

本文详细分析了一次线上系统频繁卡顿的案例,通过加入-XX:+PrintGCDetails命令定位到FullGC频繁触发的问题。深入探讨了FullGC导致的内存溢出原因,并提供了代码优化方案,包括使用StringBuilder替代String连接,合理管理对象引用,以及避免循环中大量创建大对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM知识专栏JVM-火种​​​​​​​,持续更新,喜欢请关注😍

最近线上系统出现频繁卡顿的情况,为了定位问题,在JVM参数加入了-XX:+PrintGCDetails命令(打印GC后的内存情况)。卡顿时查看控制台输出的情况,发现比正常情况多了很多Full GC,因为Full GC过程是stop the world操作,也就是整个虚拟机都要停止,所以这应该是导致卡顿的元凶了。Full GC持续了几分钟后系统抛出了异常信息,截取部分内容如下

java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:2882)
	at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:390)
	at java.lang.StringBuilder.append(StringBuilder.java:119)
	at com.project.util.TextToFile.writeText(TextToFile.java:31)
	at com.project.service.impl.JDGoodsServiceImpl.selectOrderToEmallOrderGoods(JDGoodsServiceImpl.java:993)

好在代码中加了try-catch操作,使得排查过程变得简单了。根据错误信息JDGoodsServiceImpl.selectOrderToEmallOrderGoods(JDGoodsServiceImpl.java:993)找到了对应代码,抽离业务,简化之后内容如下:

public void index() {
      for(int i = 0 ; i < 100; i++){
          writeText("插入数据");
      }
}
public static void writeText(String strBuffer) {
      try {
          File fileText = new File("D:\\fileLocalhost\\orderStatus\\orderStatusModify.txt");
          String encoding = "GBK";
          String lineTxt = "";
          if (fileText.isFile() && fileText.exists()) {
              InputStreamReader read = new InputStreamReader(
                      new FileInputStream(fileText), encoding);
              BufferedReader bufferedReader = new BufferedReader(read);
              lineTxt = bufferedReader.readLine();
          }
          if (!fileText.exists()) {
              fileText.createNewFile();
          }
          FileWriter fileWriter = new FileWriter(fileText);
          lineTxt  += strBuffer;
          fileWriter.write(lineTxt);
          fileWriter.close();
      } catch (IOException e) {
          e.printStackTrace();
      }
}

上述代码是对一个本地的txt文档做内容追加。把生产环境的orderStatusModify.txt文件拿到本地发现文件大小为14mb。将如上代码在开发环境运行后果然出现了和生成环境一样的错误。通过–XX:+PrintHeapAtGC收集到内存溢出前最后一次GC时的内存情况如下
在这里插入图片描述
图中蓝色区域是垃圾回收后新生代的内存占用情况,可以看得出新生代都被回收了,内存占用为0%。黄色区域是老年代的内存占用情况,总共2052096K(2004Mb),使用了1846963K(约1803Mb),剩余201Mb。最下方的紫色区域是永久区内存情况,reserved 1095680K即剩余约1070Mb。而此时的文档大小已经超过了老年代可用容量,所以导致内存溢出的原因是老年代空间不足。
找到报错信息中最终出错的方法at java.util.Arrays.copyOf(Arrays.java:2882),对应源码:

  public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength]; //2882行,报错代码
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

因为创建字符数组时内存空间不足,导致了内存溢出。这里引申出了一个问题:
代码怎么写可以避免这种情况?
    这段代首先需要优化的地方是字符串连接操作lineTxt += strBuffer;,String类进行大量的+操作的时候,每次+操作都是需要new一个StringBuilder的,然后使用new出来的StringBuilder进行append操作,append之后,再toString,返回新的String对象给源String对象。记住,toString在源码中是new了String对象的。所以效率低在每次new的StringBuilder和返回时new的String。每次都new大量的这种临时对象,效率自然低很多了。改用StringBuilder的话,每次减少了new操作,而是一直进行append操作。最终需要字符串的时候,再toString。效率高上百倍!
    其次因为每次创建的字符串对象在内存中不能立刻被回收,对内存有很大消耗,所以建议在结尾将对象引用置为空 lineTxt = null;。但是即使置为null,也有很多对象不会及时被回收。
    第三不建议用循环操作大对象。
最后:生产环境要用log.error(“异常方法 ,method={}”, “test”, e)代替 e.printStackTrace();
参考文章:StringBuilder的内部实现JVM参数解析

<think>嗯,用户问的是线上服务出现OutOfMemoryError: Metaspace的可能原因。首先,我需要回忆一下Metaspace的作用。Metaspace在Java 8之后取代了永久代(PermGen),主要存储类的元数据,比如类的结构信息、方法信息、常量池等。所以Metaspace的内存不足通常和类加载有关。 那导致Metaspace溢出的常见原因有哪些呢?首先想到可能是加载了太多的类。比如,应用本身如果特别大,依赖很多库,尤其是动态生成类的情况,像使用反射、CGLIB、ASM等框架,可能会生成大量代理类。这些类一旦没有被及时卸载,就会占用Metaspace的空间。 另外,动态生成类的框架,比如Spring AOP,它在使用CGLIB进行代理时,会生成很多代理类。如果应用中有大量的AOP切面,或者频繁创建代理对象,可能会导致Metaspace被占满。这时候需要检查是否有不必要的代理生成,或者调整相关配置。 还有,Metaspace的大小配置问题。默认情况下,Metaspace的大小可能不够,特别是对于大型应用。如果用户没有设置-XX:MaxMetaspaceSize,Metaspace可能会动态增长,但受限于物理内存。如果物理内存不足,或者配置的最大值太小,就容易出现OOM。这时候需要检查JVM参数,看看是否有适当调整过MaxMetaspaceSize。 类加载器泄漏也是一个可能性。如果应用服务器或者框架频繁创建和销毁类加载器,而某些类加载器没有被正确回收,那么它们加载的类就无法被卸载,导致元数据堆积。例如,OSGi环境或者某些热部署机制下,如果没有处理好类加载器的生命周期,就可能出现这种情况。 还有第三方库或框架的问题。某些库可能在内部动态生成类,比如Hibernate、MyBatis等ORM框架,或者Groovy这样的动态语言,它们可能在运行时生成字节码,增加Metaspace的使用。如果这些库存在内存泄漏,或者使用不当,也会导致问题。 此外,JVM的类元数据本身占用内存,如果元数据本身有内存泄漏,比如JVM的bug,虽然比较少见,但也不能完全排除。这时候可能需要升级JVM版本,看看是否有相关的修复。 接下来,用户可能需要排查的方法。首先检查JVM参数,确认MaxMetaspaceSize是否足够。然后,通过内存分析工具,比如JDK自带的jstat,监控Metaspace的使用情况,看是否有持续增长的趋势。或者使用堆转储工具分析加载的类,看看是否有大量重复或意外的类存在。 还有,检查应用是否有使用动态代理、反射生成类的情况,特别是循环或频繁调用的代码路径中。此外,检查类加载器的使用情况,是否存在泄漏,比如未关闭的线程类加载器,或者未清理的缓存等。 可能用户的应用在最近有新增功能,引入了新的依赖库,或者增加了动态生成类的代码,导致Metaspace的需求突然增加。这时候需要回顾最近的变更,看看是否有相关的修改。 总结下来,可能的原因包括:Metaspace配置不足、动态生成大量类、类加载器泄漏、第三方库问题、JVM bug等。排查时需要结合监控数据、JVM参数、代码审查等多方面入手。</think>线上服务出现 `OutOfMemoryError: Metaspace` 通常与类的元数据(Metadata)占用过多内存有关。以下是可能导致该问题的常见操作和场景,以及排查思路: --- ### **1. 根本原因** Metaspace 存储类的元数据(如类名、方法信息、字节码、常量池等),当以下情况发生时可能触发 OOM: - **加载过多类**:JVM 无法卸载类,导致元数据堆积。 - **元数据内存泄漏**:类加载器未释放,导致其加载的类无法被回收。 --- ### **2. 常见操作与场景** #### **(1) 动态生成大量类** - **动态代理框架**:如 CGLIB、JDK Proxy(尤其 Spring AOP 默认使用 CGLIB 时)会生成代理类。 - **脚本引擎**:Groovy、JSP 等动态编译代码的场景。 - **反射生成类**:通过 `GeneratedMethodAccessor` 或 Unsafe API 生成类。 #### **(2) 类加载器泄漏** - **未关闭的类加载器**:如 Tomcat 热部署时旧的 `WebAppClassLoader` 未被回收。 - **自定义类加载器**:未正确管理生命周期,导致加载的类无法卸载。 #### **(3) 依赖库或框架问题** - **第三方库缺陷**:如 Hibernate、MyBatis 某些版本存在类加载问题。 - **重复加载类**:框架或代码逻辑错误导致同一类被多次加载。 #### **(4) JVM 配置不当** - **`-XX:MaxMetaspaceSize` 设置过小**:默认不限制(依赖物理内存),但显式设置过小会直接触发 OOM。 - **未监控 Metaspace**:未通过监控工具(如 Prometheus + JMX)观察 Metaspace 增长趋势。 --- ### **3. 排查步骤** #### **(1) 确认 JVM 参数** 检查是否显式设置 `-XX:MaxMetaspaceSize`,例如: ```bash -XX:MaxMetaspaceSize=256m # 默认无上限,建议根据实际情况设置 ``` #### **(2) 分析类加载情况** - **使用 `jcmd` 查看加载的类数量**: ```bash jcmd <pid> GC.class_stats | grep "Total" ``` - **通过 `jstat` 监控 Metaspace 使用率**: ```bash jstat -gcmetacapacity <pid> # 输出 Metaspace 容量统计 ``` #### **(3) 检查内存泄漏** - **生成堆转储文件(Heap Dump)**: ```bash jmap -dump:live,format=b,file=heapdump.hprof <pid> ``` - **分析工具**:使用 Eclipse MAT 或 JProfiler,过滤 `ClassLoader` 对象,检查是否有冗余或未释放的类加载器。 #### **(4) 代码与框架检查** - **动态代理**:检查 CGLIB 代理是否在循环中频繁生成。 - **热部署工具**:如 JRebel、Spring Boot DevTools,确认是否导致类重复加载。 - **类加载器使用**:确保自定义类加载器在完成任务后能及时被 GC 回收。 --- ### **4. 解决方案** - **调整 Metaspace 大小**:适当增加 `-XX:MaxMetaspaceSize`。 - **优化类生成逻辑**:减少动态代理类的生成(如改用 JDK Proxy)。 - **修复类加载器泄漏**:确保自定义类加载器的生命周期可控。 - **升级依赖库**:修复已知的类加载问题(如 Spring/Hibernate 版本升级)。 --- ### **5. 预防措施** - **监控报警**:对 Metaspace 使用率设置阈值报警。 - **定期分析**:在预发环境压测,观察类加载数量和 Metaspace 增长趋势。 - **代码规范**:避免在循环或高频调用中动态生成类。 通过以上步骤,可逐步定位并解决 Metaspace OOM 问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值