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(Listlist, 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(Mapmap, 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 { Listlist = new ArrayList<>(); ListUtil.add(list, 128); Map 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.(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. (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. (Formatter.java:1892) at java.util.Formatter. (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。
至此,我们就找到了完整的调用链。