Java内存管理:方法区与常量池

总结方法区和运行时常量池

相关特征

方法区特征

  • 同 Java 堆一样, 方法区也是全局共享的一块内存区域

  • 方法区的作用是存储 Java 类的结构信息, 当我们创建对象实例后, 对象的类型信息存储在方法堆之中, 实例数据存放在堆中;实例数据指的是在 Java
    中创建的各种实例对象以及它们的值, 类型信息指的是定义在 Java
    代码中的常量、静态变量、以及在类中声明的各种方法、方法字段等等;同事可能包括即时编译器编译后产生的代码数据.

  • JVMS 不要求该区域实现自动的内存管理, 但是商用 JVM 一般都已实现该区域的自动内存管理.

  • 方法区分配内存可以不连续, 可以动态扩展.

  • 该区域并非像 JMM 规范描述的那样数据一旦放进去就属于 “永久代”; 在该区域进行内存回收的主要目的是对常量池的回收和对内存数据的卸载;一般来说这个区域的内存回收效率比起
    Java 堆要低得多.

  • 当方法区无法满足内存需求时, 将抛出 OutOfMemoryError 异常.

运行时常量池的特征

  • 运行时常量池是方法区的一部分, 所以也是全局共享的.
  • 其作用是存储 Java 类文件常量池中的符号信息.
  • class 文件中存在常量池 (非运行时常量池), 其在编译阶段就已经确定;JVM 规范对 class 文件结构有着严格的规范, 必须符合此规范的 class 文件才会被
    JVM 认可和装载.
  • 运行时常量池 中保存着一些 class 文件中描述的符号引用, 同时还会将这些符号引用所翻译出来的直接引用存储在 运行时常量池 中.
  • 运行时常量池相对于 class 常量池一大特征就是其具有动态性, Java 规范并不要求常量只能在运行时才产生, 也就是说运行时常量池中的内容并不全部来自
    class 常量池, class 常量池并非运行时常量池的唯一数据输入口;在运行时可以通过代码生成常量并将其放入运行时常量池中
  • 同方法区一样, 当运行时常量池无法申请到新的内存时, 将抛出 OutOfMemoryError 异常.

HotSpot 方法区变迁

JDK1.2 ~ JDK6

在 JDK1.2 ~ JDK6 的实现中, HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;

JDK7

由于 GC 分代技术的影响, 使之许多优秀的内存调试工具无法在 Oracle HotSpot 之上运行, 必须单独处理;并且 Oracle 同时收购了 BEA 和 Sun 公司, 同时拥有
JRockit 和 HotSpot, 在将 JRockit 许多优秀特性移植到 HotSpot 时由于 GC 分代技术遇到了种种困难, 所以从 JDK7 开始 Oracle HotSpot
开始移除永久代.

JDK7 中符号表被移动到 Native Heap 中, 字符串常量和类引用被移动到 Java Heap 中.

JDK8

在 JDK8 中, 永久代已完全被元空间 (Meatspace) 所取代.

永久代变迁产生的影响

测试代码 1

1
2
3
4
5
6
7
8
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.

产生两种结果的原因:

在 JDK6 下 s1、s2 指向的是新创建的对象, 该对象将在 Java Heap 中创建, 所以 s1、s2 指向的是 Java Heap 中的内存地址; 调用 intern
方法后将尝试在常量池中查找该对象, 没找到后将其放入常量池并返回, 所以此时 s1/s2.intern()指向的是常量池中的地址, JDK6 常量池在方法区,
与堆隔离, ;所以 s1.intern()==s1 返回 false.

测试代码 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 区,
所以不会出现异常.