在 JVM 中,除了程序计数器外,虚拟机内存中的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能,本篇就来深入剖析一下各个区域出现 OOM 异常的情形,以及如何解决各个区域的 OOM 问题。
本篇主要包括如下内容:
Java 堆用于存储对象实例,只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免 JVM 清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生溢出异常。
堆溢出复现 要复现这种情况也很简单:将 Java 堆的大小限制为固定值,且不可扩展(将堆的最小值-Xms 参数与最大值-Xmx 参数设置为一样即可避免堆自动扩展);当使用一个 while(true) 循环来不断创建对象就会发生 OutOfMemory,还可以使用 -XX:+HeapDumpOutofMemoryErorr 当发生 OOM 时会自动 dump 堆栈到文件中。
测试代码:
public static void main(String[] args) {
List<String> list = new ArrayList<>() ;
while (true){
list.add("1") ;
}
}
运行结果:
原因 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 即是说发生了堆溢出。
上面复现代码产生堆溢出的原因主要是第三点。
解决方法
运行时常量池和方法区溢出
运行时常量池是方法区的一部分,我们先对运行时常量池溢出进行测试。
运行时常量池溢出复现
最典型的使用运行时常量池的方法是 String 的 intern() 方法,该方法是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 包含的字符串添加到常量池中,并且返回此 String 对象的引用。
在 JDK1.6 及以前的版本中,由于常量池分配在永久代中,可以通过-XX:PermSize 和-XX:MaxPermSIze 限制方法区大小,从而限制其中常量池的容量
测试代码:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
笔者所用为 JDK1.8,已经去除了对这两个 JVM 参数的支持,程序执行的结果如下:
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0
暂不做深究。
方法区溢出复现
方法区用于存放 class 的相关信息,包括类名、访问修饰符、常量池、字段描述、方法描述等。可以通过借助 CGLib 直接操作字节码运行时生成大量的动态类,来填满方法区。
PermSize 和 MaxPermSize 已经不能使用了,那在 JDK1.8 中怎么设置方法区大小呢?
JDK 8 中将类信息移到了本地堆内存 (Native Heap) 中,将原有的永久代移动到了本地堆中成为 MetaSpace ,如果不指定该区域的大小,JVM 将会动态的调整。
可以使用 -XX:MaxMetaspaceSize=10M 来限制最大元空间。这样当不停的创建类时将会占满该区域并出现 OOM。
测试代码:
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer() ;
enhancer.setSuperclass(Main.class);
enhancer.setUseCache(false) ;
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o,objects) ;
}
});
enhancer.create() ;
}
}
设置好 JVM 参数后,执行上述代码,得到下面的额结果:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:530)
at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:582)
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)
at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:569)
at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:384)
at com.etekcity.cloud.Main.main(Main.java:27)
Process finished with exit code 1
这里的 OOM 伴随的是 Exception in thread "main" java.lang.OutOfMemoryError: Metaspace 也就是元空间溢出。
方法区溢出在应用中是比较常见的 OOM 异常,Spring、Hibernate 等框架在对类进行增强时,都会使用到 CGLib 技术来增强类,增强的类越多,对方法区的容量要求就越大,就越可能出现方法区的 OOM 异常。
解决方法
因为该 OOM 原因比较简单,解决方法有如下几种:
本机内存溢出
以上 OOM 异常都是出现于 JVM 内部,那么如果是机器本身分给 JVM 的内存不够导致溢出呢。
机器本身分给 JVM 的内存容量可以通过-XX:MaxDirectMemorySize 指定,如果不指定,则默认与 Java 堆最大值(-Xmx 指定一样)。
可以通过反射获取 Unsafe 实例进行内存分配,测试代码如下:
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(1024 * 1024);
}
}
运行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at Main.main(Main.19)
有 DirectMemory 导致的内存溢出,在 Heap Dump 文件中不会看到明显的异常,如果发现 OOM 之后的 dump 文件很小,可以考虑一下是否是这方面的原因。
转载自:https://www.cnblogs.com/java1024/p/12381457.html