方法区和运行时常量区溢出 转

方法区和运行时常量池溢出

由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。前面提到JDK 1.7开始逐步“去永久代”的事情,在此就以测试代码观察一下这件事对程序的实际影响。

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

代码清单2-6 运行时常量池导致的内存溢出异常

 1 /**
 2  * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 3  * @author zzm
 4  */
 5 public class RuntimeConstantPoolOOM {
 6
 7 public static void main(String[] args) {
 8 // 使用List保持着常量池引用,避免Full GC回收常量池行为
 9 List<String> list = new ArrayList<String>();
10 // 10MB的PermSize在integer范围内足够产生OOM了
11 int i = 0;
12 while (true) {
13 list.add(String.valueOf(i++).intern());
14 }
15 }
16 }  

运行结果:

  1. Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
  2. at java.lang.String.intern(Native Method)
  3. at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

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

而使用JDK 1.7运行这段程序就不会得到相同的结果,while循环将一直进行下去。关于这个字符串常量池的实现问题,还可以引申出一个更有意思的影响,如代码清单2-7所示。

代码清单2-7 String.intern()返回引用的测试

 1 public class RuntimeConstantPoolOOM {
 2
 3  public static void main(String[] args) {
 4  public static void main(String[] args) {
 5  String str1 = new StringBuilder("计算机").append("软件").toString();
 6  System.out.println(str1.intern() == str1);
 7
 8  String str2 = new StringBuilder("ja").append("va").toString();
 9  System.out.println(str2.intern() == str2);
10  } }
11 }  

这段代码在JDK 1.6中运行,会得到两个false,而在JDK 1.7中运行,会得到一个true和一个false。产生差异的原因是:在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和动态代理等),但在本次实验中操作起来比较麻烦。在代码清单2-8中,笔者借助CGLib直接操作字节码运行时生成了大量的动态类。

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

代码清单2-8 借助CGLib使方法区出现内存溢出异常

 1 /**
 2  * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 3  * @author zzm
 4  */
 5 public class JavaMethodAreaOOM {
 6
 7  public static void main(String[] args) {
 8  while (true) {
 9  Enhancer enhancer = new Enhancer();
10  enhancer.setSuperclass(OOMObject.class);
11  enhancer.setUseCache(false);
12  enhancer.setCallback(new MethodInterceptor() {
13  public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
14  return proxy.invokeSuper(obj, args);
15  }
16  });
17  enhancer.create();
18  }
19  }
20
21 static class OOMObject {
22
23 }
24 }  

运行结果:

  1. Caused by: java.lang.OutOfMemoryError: PermGen space
  2. at java.lang.ClassLoader.defineClass1(Native Method)
  3. at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
  4. at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
  5. ... 8 more

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

时间: 2024-12-23 12:19:56

方法区和运行时常量区溢出 转的相关文章

Java方法区和运行时常量池溢出问题分析

运行时常量池是方法区的一部分,方法区用于存放Class的相关信息,如类名.访问修饰符.常量池.字段描述.方法描述等. String.intern()是一个native方法,它的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象:否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用.在JDK1.6及之前版本中,由于常量池分配在永久代中(即方法区),我们可以通过-XX:PermSize和-XX:MaxPe

Java方法区和运行时常量池溢出问题分析(转)

运行时常量池是方法区的一部分,方法区用于存放Class的相关信息,如类名.访问修饰符.常量池.字段描述.方法描述等. String.intern()是一个native方法,它的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象:否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用.在JDK1.6及之前版本中,由于常量池分配在永久代中(即方法区),我们可以通过-XX:PermSize和-XX:MaxPe

第二章:方法区和运行时常量池溢出

由于运行时常量池属于方法区的一部分,因此两个区域放在一块执行. String.intern()是一个Native方法,它的作用是如果字符串常量池中已经包含了此String对象的字符串,则返回代表池中这个字符串的String对象:否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用. 可以通过以下代码测试运行时常量池溢出: public class Test { public static void main(String[] args) { int i =0; L

方法区和运行时常量池

Java虚拟机系列一:一文搞懂 JVM 架构和运行时数据区

前言 之前写博客一直比较随性,主题也很随意,就是想到什么写什么,对什么感兴趣就写什么.虽然写起来无拘无束,自在随意,但也带来了一些问题,每次写完一篇后就要去纠结下一篇到底写什么,看来选择太多也不是好事儿,更重要的是不成体系的内容对读者也不够友好.所以以后的博客尽量按系列来写,不过偶尔也会穿插其他的内容.接下来一段时间我会把写博客的重点放在 JVM (Java Virtual Machine) 和 JUC (java util concurrent ) 上,对 Java 虚拟机和 Java 并发编

一文搞懂 JVM 架构和运行时数据区 (内存区域)

前言 了解 JVM 是对 Java 开发人员的基本要求,JVM 的相关内容自然也成了现在 Java 程序员面试的重要考点.不过估计很多小伙伴和我一样,长时间醉心于 CRUD,却忘了去了解一下更底层.更基础的东西,殊不知这些才是决定你能在这条路上走多远的关键因素,那接下来我们就一起来深入学习一下看似神秘的 JVM 吧.JVM 总体来看内容还是很多的,我会把最重要的内容介绍给大家,不过如果你有时间和精力的话,还是推荐你去看一下<深入理解Java虚拟机>这本书,确实是有口皆碑.本文也会引用很多此书的

探讨:编译时常量和运行时常量

编译时常量和运行时常量 C#有2种不同的常量定义方式:编译时常量(compile-time)和运行时常量(runtime).他们的行为方式有很大的不同,如果选用了错误的一个,那么将让程序的性能和正确性产生商榷.当然,一个系统最好没有任何问题,但是如果一定会存在一个问题,那么一个稍慢的但强壮的系统比一个速度更快的但很脆弱的系统要好.基于以上理由,在二者选其一的时候,你最好选择运行时常量. 运行时常量以 readonly 关键字定义,编译时是常量以 const 关键字定义: public const

c语言—栈区,堆区,全局区,文字常量区,程序代码区 详解

转:http://www.cnblogs.com/xiaowenhui/p/4669684.html 一.预备知识-程序的内存分配 一个由C/C++编译的程序占用的内存分为以下几个部分1.栈区(stack)- 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈.2.堆区(heap) - 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 .注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵.3.全局区(静态区)(static)-,全局

内存管理:栈区,堆区,全局区,文字常量区,程序代码区

一.预备知识-程序的内存分配 一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)- 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其 操作方式类似于数据结构中的栈. 2.堆区(heap) - 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回 收 .注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵. 3.全局区(静态区)(static)-,全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变