1 直接内存溢出
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域。但这部分内存也被频繁的使用,而且也可能导致 OutOfMemoryError 异常出现。
JDK1.4 中新引入了 NIO 机制,它是一种基于通道与缓冲区的新 I/O 方式,可以直接从操作系统中分配直接内存,即直接堆外分配内存,这样能在一些场景中提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
2 内存溢出
内存溢出(OutOfMemory,简称OOM)是一个令人头疼的问题,它通常出现在某一块内存空间耗尽的时候。在 Java 程序中,导致内存溢出的原因有很多,其中最常见的有:堆溢出、直接内存溢出、方法区溢出等。
2.1 堆溢出
这种场景最为常见,报错信息:
java.lang.OutOfMemoryError: Java heap space
原因
1、代码中可能存在大对象分配
2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
解决方法
1、检查是否存在大对象的分配,最有可能的是大数组分配
2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
2.2.1 堆溢出案例
//-Xmx60M -Xms60M public class SimpleHeapOOM { public static void main(String[] args) { List<String[]> list = new ArrayList<>(); for (int i = 0; i < 2000; i++) { list.add(new String[1024*1024]); } } }
执行上述代码报错:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.msdn.java.hotspot.gc.SimpleHeapOOM.main(SimpleHeapOOM.java:16)
由于堆空间不可能无限增长,所以我们需要借助前文提到的 MAT 或 VIsualVM 工具,分析 dump 文件(使用-XX:+HeapDumpOnOutOfMemoryError命令生成 dump文件),弄清楚到底是出现了内存泄漏(Memory Leak) 还是内存溢出(Memory Overflow)。
如果是内存泄漏, 可进一步通过工具查看泄漏对象到 GC Roots 的引用链, 找到泄漏对象是通过怎样的引用路径、 与哪些 GC Roots 相关联, 才导致垃圾收集器无法回收它们, 根据泄漏对象的类型信息以及它到 GC Roots 引用链的信息, 一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查 Java 虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
2.3 永久代或元空间溢出
方法区和运行时常量池的溢出
-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小) -XX:MaxMetaspaceSize=N //设置Metaspace的最大大小,默认为-1,即不限制, 或者说只受限于本地内存大小
与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
那么如何演示方法区溢出呢?首先我们明确方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、 方法描述等。对于这部分区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出为止。本文借助 CGLib 直接操作字节码运行时生成了大量的动态类。
报错信息:java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace
原因
永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。
JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:
字符串常量由永久代转移到堆中
和永久代相关的JVM参数已移除
可能原因有如下几种:
1、在Java7之前,频繁的错误使用String.intern()方法
2、运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
3、应用长时间运行,没有重启
没有重启 JVM 进程一般发生在调试时,如下面 tomcat 官网的一个 FAQ:
Why does the memory usage increase when I redeploy a web application? That is because your web application has a memory leak. A common issue are “PermGen” memory leaks. They happen because the Classloader (and the Class objects it loaded) cannot be recycled unless some requirements are met (). They are stored in the permanent heap generation by the JVM, and when you redeploy a new class loader is created, which loads another copy of all these classes. This can cause OufOfMemoryErrors eventually. (*) The requirement is that all classes loaded by this classloader should be able to be gc’ed at the same time.
解决方法
因为该OOM原因比较简单,解决方法有如下几种:
1、检查是否永久代空间或者元空间设置的过小
2、检查代码中是否存在大量的反射操作
3、dump之后通过mat检查是否存在大量由于反射生成的代理类
4、放大招,重启JVM
GC overhead limit exceeded
这个异常比较的罕见,报错信息:
java.lang.OutOfMemoryError:GC overhead limit exceeded
原因
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
解决方法
1、检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
2、添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
3、dump内存,检查是否存在内存泄露,如果没有,加大内存。
2.3.1 永久代或元空间溢出案例
//-XX:MaxMetaspaceSize=10M public class RuntimeConstantPoolOOM { static class OOMObject{ } public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o, args); } }); enhancer.create(); } } }
执行结果为:
Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:557) at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
那么在实际应用中都有哪些场景可能出现方法区溢出呢?
1、关于上述 CGLib 操作字节码生成动态类的代码,在当前很多主流框架,如 Spring、 Hibernate 对类进行增强时, 都会使用到 CGLib 这类字节码技术,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。
2、除此之外,很多运行于Java虚拟机上的动态语言(例如Groovy等)通常都会持续创建新类型来支撑语言的动态性,随着这类动态语言的流行,方法区溢出的情况也会更加频繁。
3、大量 JSP 或动态产生 JSP 文件的应用(JSP第一次运行时需要编译为Java类)、基于 OSGi 的应用(即使是同一个类文件, 被不同的加载器加载也会视为不同的类) 等。
2.4 栈溢出
报错信息:
java.lang.OutOfMemoryError : unable to create new native Thread
原因
出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。
解决方法
1、通过 -Xss 降低的每个线程栈大小的容量
2、线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
/proc/sys/kernel/pid_max
/proc/sys/kernel/thread-max
maxuserprocess(ulimit -u)
/proc/sys/vm/maxmapcount
由于 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,栈容量只能由-Xss参数来设定。 关于虚拟机栈和本地方法栈, 在《Java虚拟机规范》 中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度, 将抛出 StackOverflowError 异常。
如果虚拟机的栈内存允许动态扩展, 当扩展栈容量无法申请到足够的内存时, 将抛出 OutOfMemoryError 异常。
《Java虚拟机规范》 明确允许Java虚拟机实现自行选择是否支持栈的动态扩展, 而 HotSpot 虚拟机的选择是不支持扩展, 所以除非在创建线程申请内存时就因无法获得足够内存而出现 OutOfMemoryError 异常, 否则在线程运行时是不会因为扩展而导致内存溢出的, 只会因为栈容量无法容纳新的栈帧而导致 StackOverflowError 异常。
栈溢出抛出 StackOverflowError 错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。一般情况下是程序错误所致的,比如写了一个死递归,就有可能造成此种情况。下面我们通过一段代码来模拟一下此种情况的内存溢出。
2.4.1 栈溢出案例
public class StackOOM { private int len = 1; public void stackOverFlowMethod() { len++; stackOverFlowMethod(); } public static void main(String[] args) { StackOOM stackOOM = new StackOOM(); try { stackOOM.stackOverFlowMethod(); } catch (Throwable e) { System.out.println("stack length: " + stackOOM.len); throw e; } } }
执行结果为:
stack length: 17850 Exception in thread "main" java.lang.StackOverflowError
那么 HotSpot 虚拟机中是否可以抛出 OutOfMemoryError 呢?我们尝试如下两种方案:
方案一:使用-Xss 参数减少栈内存容量,JDK8 要求栈内存容量至少为 160K,针对上述代码设置如下参数:-Xss160k,执行结果变为:
stack length: 774 Exception in thread "main" java.lang.StackOverflowError
经过测试发现,栈内存容量变小,仍然会抛出 StackOverflowError 异常,异常出现时输出的堆栈深度相应缩小。
方案二:在方法中多定义一些局部变量,增大方法栈中拘捕变量表的长度。
修改代码如下:
public void stackOverFlowMethod() { long l1, l2, l3, l4, l5, l6, l7, l8, l9; l1 = l2 = l3 = l4 = l5 = l6 = l7 = l8 = l9 = 100L; double d1, d2, d3, d4, d5, d6, d7, d8, d9; d1 = d2 = d3 = d4 = d5 = d6 = d7 = d8 = d9 = 100.0; len++; stackOverFlowMethod(); }
执行结果为:
stack length: 4817 Exception in thread "main" java.lang.StackOverflowError
根据结果可知,同样抛出 StackOverflowError 异常, 异常出现时输出的堆栈深度相应缩小。
关于这句话“在 HotSpot 虚拟机上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常——只要线程申请栈空间成功了就不
会有 OOM, 但是如果申请时就失败, 仍然是会出现 OOM 异常的”是什么意思呢?我们上面尝试的两个方案没有出现 OOM 异常,那是因为程序执行过程中线程数是固定的,所以只要线程申请空间成功,即开始执行,就不会有 OOM。那么我们尝试在代码中不断创建新的线程,看看结果如何。
每个线程的开启都要占用栈内存,如果线程数量过大,栈空间使用完毕,也有可能导致 OOM。如下案例所示:
//-Xmx1g public class MultiThreadOOM { public static class SleepThread implements Runnable { @Override public void run() { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { for (int i = 0; i < 5000; i++) { new Thread(new SleepThread(), "Thread" + i).start(); System.out.println("Thread" + i); } } }
执行结果报错:
Thread4067 Thread4068 Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method)
注意,执行上述代码记得先保存当前的工作,Java 的线程是映射到操作系统的内核线程上,创建线程数过多可能对操作系统带来压力,针对上述 OOM 异常,即使使用 -Xss 参数减少栈内存容量,仍然会报相同的错误,又或者使用 -Xmx 降低堆内存大小,结果也样。说明当前系统仅支持创建 4069 个线程。
2.5 非常规溢出
下面这些OOM异常,可能大部分的同学都没有碰到过,但还是需要了解一下
分配超大数组
报错信息 :
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。
解决方法就是检查你的代码中是否有创建超大数组的地方。
swap溢出
报错信息 :
java.lang.OutOfMemoryError: Out of swap space
这种情况一般是操作系统导致的,可能的原因有:
1、swap 分区大小分配不足;
2、其他进程消耗了所有的内存。
解决方案:
1、其它服务进程可以选择性的拆分出去 2、加大swap分区大小,或者加大机器内存大小
本地方法溢出
报错信息 :
java.lang.OutOfMemoryError: stack_trace_with_native_method
本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处。
这个异常出现的概率极低,只能通过操作系统本地工具进行诊断,难度有点大,还是放弃为妙。