前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实战:OutOfMemoryError 异常(三) -- 方法区和运行时常量池溢出

实战:OutOfMemoryError 异常(三) -- 方法区和运行时常量池溢出

作者头像
Li_XiaoJin
发布2022-06-10 21:04:58
2030
发布2022-06-10 21:04:58
举报
文章被收录于专栏:Lixj's BlogLixj's Blog

关于方法区和运行时常量池溢出的情况。

运行时常量池是方法区的一部分,所以这两个放到一起进行测试。

String.intern() 是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。在JDK 1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制方法区大小,从而间接限制其中常量池的容量。

代码如下:

代码语言:javascript
复制
/**
 * -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

运行结果:

运行时常量池溢出,在 OutOfMemoryError 后面跟随的提示信息 是“PermGen space”,说明运行时常量池属于方法区(HotSpot 虚拟机中的永久代)的一部 分。

而当使用 JDK 1.7(或者 JDK 1.8)运行这段程序就不会得到相同的结果,while 循环将一直进行下去。关于这个字符串常量池的实现问题,还可以引申出一个更有意思的影响,

代码语言:javascript
复制
public class RuntimeConstantPoolOOMNew {
    public static void main(String[] args) {
        String str1 = new StringBuilder("我爱你").append("中国").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("hello").append("world").toString();
        System.out.println(str2.intern() == str2);
    }
}

这段代码在 JDK 1.6 中运行,会得到两个 false,而在 JDK 1.7 中运行,会得到一个 true 和一 个 false。(在JDK 1.8 中运行,会得到两个 true)

产生差异的原因是:在 JDK 1.6 中,intern() 方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由 StringBuilder 创建的字符串 实例在 Java 堆上,所以必然不是同一个引用,将返回 false。而 JDK 1.7(以及部分其他虚拟机,例如 JRockit)的 intern() 实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此 intern() 返回的引用和由 StringBuilder 创建的那个字符串实例是同一个。对 str2 比 较返回 false 是因为 “java” 这个字符串在执行 StringBuilder.toString() 之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用Java SE API也可以动态产生类(如反射时的 GeneratedConstructorAccessor 和动态代理等),但在本次实验中操作起来比较麻烦。在以下代码中,通过借助 CGLib 直接操作字节码运行时生成了大量的动态类。

值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经常会出现在实际应用中:当前的很多主流框架,如 Spring、Hibernate,在对类进行增强时,都会使用到 CGLib 这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的 Class 可以加载入内存。另外,JVM 上的动态语言(例如 Groovy 等)通常都会持续创建类来实现语言的动态性,随着这类语言的流行,也越来越容易遇到类似的溢出场景。

代码如下:

代码语言:javascript
复制
/**
 * -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class JavaMethodAreaOOM {
    static class OOMObject {

    }
    public static void main(String[] args) {
        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.invoke(o, args);
                }
            });
            enhancer.create();
        }
    }
}

运行结果:

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量 Class 的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了 CGLib 字节码增强和动态语言之外,常见的还有:大量 JSP 或动态产生 JSP 文件的应用(JSP 第一次运行时需要编译为 Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

感觉看完还是有点迷~

书籍介绍:《深入理解Java虚拟机:JVM高级特性与最佳实践》

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可 Links: https://lixj.fun/archives/实战outofmemoryerror异常三--方法区和运行时常量池溢出

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-01-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档