Java OOM异常

OOM:Out Of Memory,就是常说的内存溢出。

出现原因

堆内存分配过低、代码问题如:死循环、资源未关闭、对象过大或者未及时回收等。

举例分析

堆内存分配过低

解决办法自然就是加大堆空间。

-Xmx:最大堆大小

代码问题

我们写个demo分析下。

OOMObject.java

package com.boot.demo.test.jvm;

/**
 * @author braska
 * @date 2020/3/18
 **/
public class OOMObject {
    public byte[] bytes = new byte[64 * 1024];
} 

ListUtil.java

package com.boot.demo.test.jvm;

import java.util.List;

/**
 * @author braska
 * @date 2020/3/18
 **/
public class ListUtil {

    public static void add(List<OOMObject> list, int num) throws Exception {
        for (int i = 0; i < num; i++) {
            list.add(new OOMObject());
        }
    }
}

MapUtil.java

package com.boot.demo.test.jvm;

import java.util.Map;

/**
 * @author braska
 * @date 2020/3/18
 **/
public class MapUtil {

    public static void put(Map<String, Object> map, int num) {
        for (int i = 0; i < num; i++) {
            map.put(String.format("key%s", i), new OOMObject());
        }
    }
}

DumpTest.java

package com.boot.demo.test.jvm;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * jvm启动参数  -Xmx10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d://
 *
 * @author braska
 * @date 2020/3/18
 **/
public class DumpTest {

    public static void main(String[] args) throws Exception {
        List<OOMObject> list = new ArrayList<>();
        ListUtil.add(list, 128);

        Map<String, Object> map = new HashMap<>();
        MapUtil.add(map, 2);
        while (!Thread.interrupted()) {
            Thread.sleep(100);
        }
    }
}

控制台输出:

Connected to the target VM, address: '127.0.0.1:64752', transport: 'socket'
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to d://java_pid10640.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.util.HashMap.newNode(HashMap.java:1742)
	at java.util.HashMap.putVal(HashMap.java:641)
	at java.util.HashMap.put(HashMap.java:611)
	at java.util.HashSet.add(HashSet.java:219)
Heap dump file created [10485144 bytes in 0.276 secs]
	at sun.util.locale.provider.JRELocaleProviderAdapter.createLanguageTagSet(JRELocaleProviderAdapter.java:373)
	at sun.util.locale.provider.JRELocaleProviderAdapter.getLanguageTagSet(JRELocaleProviderAdapter.java:349)
	at sun.util.locale.provider.JRELocaleProviderAdapter.getCurrencyNameProvider(JRELocaleProviderAdapter.java:224)
	at sun.util.locale.provider.JRELocaleProviderAdapter.getLocaleServiceProvider(JRELocaleProviderAdapter.java:100)
	at sun.util.locale.provider.LocaleServiceProviderPool.<init>(LocaleServiceProviderPool.java:133)
	at sun.util.locale.provider.LocaleServiceProviderPool.getPool(LocaleServiceProviderPool.java:111)
	at java.util.Currency.getSymbol(Currency.java:506)
	at java.text.DecimalFormatSymbols.initialize(DecimalFormatSymbols.java:648)
	at java.text.DecimalFormatSymbols.<init>(DecimalFormatSymbols.java:113)
	at sun.util.locale.provider.DecimalFormatSymbolsProviderImpl.getInstance(DecimalFormatSymbolsProviderImpl.java:85)
	at java.text.DecimalFormatSymbols.getInstance(DecimalFormatSymbols.java:180)
	at java.util.Formatter.getZero(Formatter.java:2283)
	at java.util.Formatter.<init>(Formatter.java:1892)
	at java.util.Formatter.<init>(Formatter.java:1914)
	at java.lang.String.format(String.java:2940)
	at com.boot.demo.test.jvm.MapUtil.add(MapUtil.java:13)
	at com.boot.demo.test.jvm.DumpTest.main(DumpTest.java:21)
Disconnected from the target VM, address: '127.0.0.1:64752', transport: 'socket'

Process finished with exit code 1

有人可能会疑惑。这么简单一个demo,为什么要拆分这么多各类呢?

看上面这个会抛出OOM异常的demo,看我们很容易看出产生OOM的主因其实是因为ListUtil的add方法添加了过多的对象,而MapUtil的put操作其实只是压死骆驼的最后一根稻草。但是看控制台的输出,你会发现错误堆栈只能跟踪到MapUtil这边。

这就是分这么多类的目的。生产环境中,我们的代码量往往是这个demo的成千上百倍。我们又常常根据模块、业务拆分类、包名,使得OOM异常的追踪排查并不会一眼就能看出,日志的输出也不一定准确。

所以如果你在看完日志后依然找不出OOM原因的时候,你应该学会如何分析程序的dump文件。

生成dump日志

 启动前

  程序发生OOM时,会自动生成java_pid.hprof文件

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=dump/

 运行中

  如果你在应用程序启动前,忘了加jvm参数。没关系jmap命令可以帮助你生成dump快照。

jmap -dump:format=b,file=dump/ pid

  pid的查询方式有很多。linux下,我们可以通过netstat、ps、jps等命令获取到pid。这里我就不细写了。

分析dump日志

  当我们拿到程序的dump快照时,我们可以使用jdk自带的jhat工具、eclipse的mat工具或者jprofiler工具进行分析。前两者免费,jprofiler收费。

  我们用jprofilter打开hprof文件,选择Biggest Objects。会看到ArrayList对象占用了88%的堆内存。

 从上面我们可以看出,事实上,占用大量堆空间的主谋其实是ArrayList,HashMap只是推手而已。

可是,ArrayList家户喻晓,使用率那么高,你凭什么说是我写的代码导致的OOM。万一是别人的代码有问题呢?

这时候,我们可以右键ArrayList对象,选择Use Select Objects。跳转到References页签。Outgoing references表示调用了哪些对象。Incoming references表示谁引用了这个大对象。我们选Incoming References。

至此,我们就找到了完整的调用链。

原文地址:https://www.cnblogs.com/braska/p/12518278.html