Java程序员必看:如何解决OOM(Out Of Memory)问题及堆溢出(heap)的调试技巧
认真写文章,用心做分享。公众号:Java耕耘者 文章都会在里面更新,整理的资料也会放在里面。
作为Java程序员,我们除了享受垃圾回收机制带来的便利之外,还深受OOM(Out Of Memory)的困惑和折磨。
我们先看一下java的内存分配情况
堆溢出(堆)
编写以下例程:
公共静态无效主(字符串[] args){
列表数据 = new ArrayList();
而(真){
datas.add(新字节[1024 * 1024]);
同时,设置虚拟机参数,以便java进程能够快速生成heapdump。
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
注释:设置堆内存限制为20m,Xms/Xmx分别对应堆的初始堆大小和最大堆大小。
产生的异常信息:
记忆分析垫
点评:从这里可以看出,这里聚集的对象很多,占据了99%的内存。
堆栈溢出(堆栈)
堆栈溢出异常仍然具有明确的方向性。我暂时忽略它们。
永久代溢出(常量池/方法区)
String.intern() 和 cglib 动态代理类的两个示例。
该错误也非常具体:
线程“main”中的异常 java.lang.OutOfMemoryError: PermGen space
给大家讲一下小编(mumuxinfei)很久以前做过的一个项目。
OOM原因分类
导致程序OOM的因素有很多。一般来说,我们可以简单地将OOM分为堆溢出(Heap)、栈溢出(Stack)、永久代溢出(常量池/方法区)、直接内存溢出。
outOfMemoryError 老年代内存不足。 outOfMemoryError: PermGen Space 永久代内存不足。 outOfMemoryError: GC Overhead Limit Exceed 垃圾收集时间占用了系统运行时间的98%以上。
这里我们需要结合代码观察是否有大量应该回收的对象不断被引用或者是否存在占用内存特别大而无法回收的对象。
问题展示
日志档案:
时间戳 [http-nio-端口号-exec-1] ERROR ResponseExceptionHandle:26 - Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: GC overhead limit exceeded
org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: GC overhead limit exceeded
****中间一大段****
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded
这时候如果有可视化界面就好了(或者如果你有运维电脑的话可以自己用Arthas上去)。没有办法,只能下载运维线程堆栈信息和内存信息。
在Linux上生成文件命令
# 生成堆转储快照dump文件命令
jmap -dump:format=b,file=heapdump.hprof pid
# 生成堆栈信息文件命令
jstack -l pid >> stack.txt
查看堆栈信息
"http-nio-端口号-exec-1" #37 daemon prio=5 os_prio=0 tid=0x00007f789d236800 nid=0x650e waiting on condition [0x00007f7851409000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000faa51ba0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None
****等等容器内其他线程
上面只是一个容器线程状态视图,我截取了整个线程堆栈信息。整个文件中,没有发现死锁。线程没有问题。不会有线程竞争资源,导致内部服务请求被阻塞,造成OOM。情况发生。
堆转储快照查看
我们使用上面的指令将 Java 堆以 hprof 二进制格式转储到 filename 指定的文件中。用普通文件方式打开肯定不行(你在想什么?醒醒吧)。我有两种方法来处理和分析这种文件。
使用jhat
使用JDK中的指令来分析文件(有时转储的堆很大,启动时会报堆空间不足的错误。可以添加参数:jhat -J-Xmx512m。内存大小可以根据需要设置到您自己的计算机。):
然后就可以在浏览器中输入:7000/来访问:
当前页面显示当前容器中包含的类和ID。页面底部是导航栏:
一般来说,在检查堆异常时,我们主要看这两部分:
需要注意的是,一般不会直接在服务器上进行分析,因为jhat是一个耗时且消耗硬件资源的过程。服务器生成的转储文件一般会复制到本地或其他机器上进行分析。
使用内存分析器
打开mat(在MAC上安装MAT)并导入文件:
这里解释了很多参数
浅堆表示对象消耗的内存总量。对象X的Retained Set是指X被垃圾回收后将被GC回收的对象的集合。对象X的Retained heap是指X的retained集中所有对象的浅堆之和,或者说因为对象X而保持活动的内存大小。
我们看看下面的选项:
直方图可以列出内存中的对象、对象的数量和大小。支配树可以列出该线程以及该线程下的对象所占用的空间。顶级消费者以图形方式列出最大的对象。 Leak Suspects 通过 MA 自动分析泄漏原因。
我们只看最大的区域(占用41MB):我们可以看到显示对象的名称在左下角:
org.hibernate.internal.SessionFactoryImpl。我们的技术栈持久层框架使用hibernate
我们点击Leak Suspects,然后:
MAT之后,出现了两个与疑似内存泄漏相关的问题:
我们通过查看下面的问题描述就知道了这一点:
Question1:LaunchedURLClassLoader类加载器加载的SessionFactoryImpl实例中的BoundedConcurrentHashMap累计内存空间,占用42,984,656字节。
Question2:这块内存中有SystemClassLoader加载的Class对象7,941个,占用10,075,768字节。