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

最近线上系统出现频繁卡顿的情况,为了定位问题,在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,也有很多对象不会及时被回收。
    第三不建议用循环操作大对象。
参考文章:StringBuilder的内部实现JVM参数解析

发布了56 篇原创文章 · 获赞 67 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/leo187/article/details/89636227