剖析 java outofmemoryerror 异常 · 物联网平台-威尼斯人最新

so_cool · 2020年03月08日 · 最后由 回复于 2022年09月03日 · 376 次阅读

在 jvm 中,除了程序计数器外,虚拟机内存中的其他几个运行时区域都有发生 outofmemoryerror 异常的可能,本篇就来深入剖析一下各个区域出现 oom 异常的情形,以及如何解决各个区域的 oom 问题。

本篇主要包括如下内容:

  • java 堆溢出
  • 运行时常量池和方法区溢出
  • 本地内存溢出
    java 堆溢出

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 即是说发生了堆溢出。

  1. 代码中可能存在大对象分配 ;
  2. 可能存在内存泄露,导致在多次 gc 之后,还是无法找到一块足够大的内存容纳当前对象;
  3. 如果不是以上两种情况,也就是说内存中的对象都必须存活,就应当检查虚拟机的堆参数(-xmx 与-xms),是否设置的堆内存空间太小,以及检查代码中是否存在某些对象声明周期过长、持有状态时间过长的情况。

上面复现代码产生堆溢出的原因主要是第三点。

解决方法

  1. 检查是否存在大对象的分配,最有可能的是大数组分配;
  2. 通过 jmap 命令,把堆内存 dump 下来,使用 mat 工具分析一下,检查是否存在内存泄露的问题
  3. 如果没有找到明显的内存泄露,使用 -xmx 加大堆内存;
  4. 还有一点容易被忽略,检查是否有大量的自定义的 finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性。

运行时常量池和方法区溢出
运行时常量池是方法区的一部分,我们先对运行时常量池溢出进行测试。

运行时常量池溢出复现
最典型的使用运行时常量池的方法是 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 原因比较简单,解决方法有如下几种:

  1. 检查是否永久代空间或者元空间设置的过小;
  2. 检查代码中是否存在大量的反射操作;
  3. dump 之后通过 mat 检查是否存在大量由于反射生成的代理类;
  4. 重启 jvm。

本机内存溢出
以上 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 文件很小,可以考虑一下是否是这方面的原因。
转载自:

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册
网站地图