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

本文深入探讨了方法区和运行时常量池溢出问题,通过具体代码示例展示了如何在JDK1.6及JDK1.7版本下观察并理解内存溢出异常的不同表现。同时,文章还介绍了如何利用CGLib等技术导致方法区溢出,并分析了溢出现象在实际应用中的常见场景,如动态生成类的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。前面提到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的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

转载于:https://www.cnblogs.com/frankfang1/p/3989934.html

### JVM 方法区运行常量池的工作机制与作用 #### 1. 运行常量池的概念 运行常量池(Runtime Constant Pool)是方法区的一部分,它用于存储类文件中的常量信息。这些信息包括但不限于字面量(literals)、符号引用(symbolic references),以及通过某些方式动态生成的常量[^2]。 #### 2. 运行常量池的作用 运行常量池的主要作用在于支持程序运行所需的各类常量资源。具体来说: - 它保存了由编译器生成的各种字面量符号引用。 - 提供了一个可扩展的空间,允许在运行期间动态加入新创建的常量,比如通过 `String.intern()` 方法产生的字符串实例[^3]。 #### 3. 动态性特点 与其他类型的常量池相比,运行常量池的一个显著特点是其 **动态性**。这意味着不仅限于预先定义好的常量可以存入其中,在程序执行过程中还可以新增加一些未预料到的数据项。例如当调用 `String.intern()` 函数,如果该字符串尚未存在于字符串常量池,则会将其添加进去。 #### 4. 存储内容 运行常量池所包含的具体内容有以下几个方面: - 字面量:如整数、浮点数等基本数据类型值; - 符号引用:指向其他对象或者方法的名字及其描述符; - 动态生成的新常量:像上述提到过的经由`intern()`操作引入的字符串实例。 #### 5. 可能引发的问题——内存溢出 由于运行常量池位于方法区内,因此它的大小受到整个方法区域容量的影响。一旦超出设定界限就会抛出OutOfMemoryError错误提示。这种情况可能发生在频繁加载卸载大量临性的class文件或是滥用 intern() 方法导致过多独一无二的字符串驻留等情况之下[^4]。 ```java // 示例代码展示如何可能导致 OOM 错误 public classOOMExample { public static void main(String[] args){ Set<String> set = new HashSet<>(); try{ while(true){ String str = UUID.randomUUID().toString(); set.add(str.intern()); } }catch(OutOfMemoryError e){ System.err.println("Out of Memory Error!"); } } } ``` 此段代码不断生成随机UUID并将它们换成interned形式存放到HashSet里头,最终因为持续增加的独特字符串填满运行常量池而导致 OutOfMemoryError 发生。 #### 总结 综上所述,JVM 中的方法区内的运行常量池是一个非常重要的组件,负责管理应用程序运行所需的一系列静态及动态生成的常量资料。了解其工作机制对于优化性能、预防潜在风险具有重要意义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值