转载于https://mritd.me/2016/03/22/Java-%E5%86%85%E5%AD%98%E4%B9%8B%E6%96%B9%E6%B3%95%E5%8C%BA%E5%92%8C%E8%BF%90%E8%A1%8C%E6%97%B6%E5%B8%B8%E9%87%8F%E6%B1%A0/
一、相关特征
1、方法区特征
- 同 Java 堆一样,方法区也是全局共享的一块内存区域
- 方法区的作用是存储 Java 类的结构信息,当我们创建对象实例后,对象的类型信息存储在方法堆之中,实例数据存放在堆中;实例数据指的是在 Java 中创建的各种实例对象以及它们的值,类型信息指的是定义在 Java 代码中的常量、静态变量、以及在类中声明的各种方法、方法字段等等;同事可能包括即时编译器编译后产生的代码数据。
- JVMS 不要求该区域实现自动的内存管理,但是商用 JVM 一般都已实现该区域的自动内存管理。
- 方法区分配内存可以不连续,可以动态扩展。
- 该区域并非像 JMM 规范描述的那样数据一旦放进去就属于 “永久代”;在该区域进行内存回收的主要目的是对常量池的回收和对内存数据的卸载;一般来说这个区域的内存回收效率比起 Java 堆要低得多。
- 当方法区无法满足内存需求时,将抛出 OutOfMemoryError 异常。
2、运行时常量池的特征
- 运行时常量池是方法区的一部分,所以也是全局共享的。
- 其作用是存储 Java 类文件常量池中的符号信息。
- class 文件中存在常量池(非运行时常量池),其在编译阶段就已经确定;JVM 规范对 class 文件结构有着严格的规范,必须符合此规范的 class 文件才会被 JVM 认可和装载。
- 运行时常量池 中保存着一些 class 文件中描述的符号引用,同时还会将这些符号引用所翻译出来的直接引用存储在 运行时常量池 中。
- 运行时常量池相对于 class 常量池一大特征就是其具有动态性,Java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池中的内容并不全部来自 class 常量池,class 常量池并非运行时常量池的唯一数据输入口;在运行时可以通过代码生成常量并将其放入运行时常量池中。
- 同方法区一样,当运行时常量池无法申请到新的内存时,将抛出 OutOfMemoryError 异常。
二、HotSpot 方法区变迁
1、JDK1.2 ~ JDK6
在 JDK1.2 ~ JDK6 的实现中,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;
2、JDK7
由于 GC 分代技术的影响,使之许多优秀的内存调试工具无法在 Oracle HotSpot之上运行,必须单独处理;并且 Oracle 同时收购了 BEA 和 Sun 公司,同时拥有 JRockit 和 HotSpot,在将 JRockit 许多优秀特性移植到 HotSpot 时由于 GC 分代技术遇到了种种困难,所以从 JDK7 开始 Oracle HotSpot 开始移除永久代。
JDK7中符号表被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。
3、JDK8
在 JDK8 中,永久代已完全被元空间(Meatspace)所取代。
三、永久代变迁产生的影响
1、测试代码1
public class Test1 {
public static void main(String[] args) {
String s1 = new StringBuilder("漠").append("然").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("漠").append("然").toString();
System.out.println(s2.intern() == s2);
}
}
以上代码,在 JDK6 下执行结果为 false、false,在 JDK7 以上执行结果为 true、false。
首先明确两点: 1、在 Java 中直接使用双引号展示的字符串将会在常量池中直接创建。 2、String 的 intern 方法首先将尝试在常量池中查找该对象,如果找到则直接返回该对象在常量池中的地址;找不到则将该对象放入常量池后再返回其地址。JDK6 常量池在方法区,频繁调用该方法可能造成 OutOfMemoryError。
产生两种结果的原因:
String.intern()在JDK1.6中,会先判断常量池中是否存在当前字符串,不存在就会将当前字符串复制到常量池,并返回常量池中字符串的引用。
而JDK1.7以后,会先判断常量池中是否存在当前字符串,不存在时不会将当前字符串复制到常量池,只是当前字符串的引用。如果存在,也不会改变存在的引用,以及只会保存首次存储的字符串的引用。如上代码所示,常量池保存的是str1的引用,即使str2调用intern()方法,返回的也是str1的引用。
从以上代码也可以看出,调用toString方法只会在堆中创建字符串对象。
字符串创建各种状态如下:
1.只在常量池上创建常量
String a1 = "AA";
2.只在堆上创建对象:调用toString方法或者+
String a2 = new String("A") + new String("A");
3.在堆上创建对象,在常量池上创建常量
String a3 = new String("AA");
4.在堆上创建对象,在常量池上创建引用
String a4 = new String("A") + new String("A");//只在堆上创建对象AA
a4.intern();//将该对象AA的引用保存到常量池上
2、测试代码2
public class Test2 {
public static void main(String[] args) {
/**
* 首先设置 持久代最大和最小内存占用(限定为10M)
* VM args: -XX:PermSize=10M -XX:MaxPremSize=10M
*/
List<String> list = new ArrayList<String>();
// 无限循环 使用 list 对其引用保证 不被GC intern 方法保证其加入到常量池中
int i = 0;
while (true) {
// 此处永久执行,最多就是将整个 int 范围转化成字符串并放入常量池
list.add(String.valueOf(i++).intern());
}
}
}
以上代码在 JDK6 下会出现 Perm 内存溢出,JDK7 or high 则没问题。
原因分析:
JDK6 常量池存在方法区,设置了持久代大小后,不断while循环必将撑满 Perm 导致内存溢出;JDK7 常量池被移动到 Native Heap(Java Heap),所以即使设置了持久代大小,也不会对常量池产生影响;不断while循环在当前的代码中,所有int的字符串相加还不至于撑满 Heap 区,所以不会出现异常。