【Java】Java永久代:从诞生到终结的演进史
Java永久代(Permanent Generation,简称 PermGen)是 Java 虚拟机(JVM)在JDK 7 及以前版本中用于实现方法区的一个内存区域。
理解永久代,最核心的一句话是:永久代是 HotSpot JVM 对“方法区”这一 JVM 规范的具体实现。(就像堆是 Java 对象存放的地方一样,永久代是类元数据存放的地方)。
以下是关于永久代的详细解析:
1. 永久代里存了什么?
在 JDK 7 之前,永久代主要存储以下信息:
- 类元数据:类的结构、字段、方法、构造函数等字节码信息。
- 运行时常量池:包括类编译后的字面量和符号引用。
- 字符串常量池:存储程序中的字符串字面量。
- 静态变量:类级别的变量。
- JIT 编译后的代码:即时编译器编译后的本地机器码(部分情况下)。
- 类加载器信息:包括系统类加载器和自定义类加载器的引用。
2. 永久代为什么被移除?(痛点在哪里?)
在 JDK 8 中,Oracle 正式完全移除了永久代,将其替换为元空间。永久代被移除的主要原因包括:
- 内存大小固定,容易发生 OOM:
永久代的大小在启动时通过-XX:MaxPermSize指定,一旦设定很难动态调整。如果在运行时加载了大量的类(例如使用大量动态代理、反射、JSP重编译的Web应用),很容易抛出著名的错误:java.lang.OutOfMemoryError: PermGen space。 - 垃圾回收效率低:
永久代的垃圾回收(Full GC)条件苛刻,主要针对常量池中的无用常量和卸载无用的类。由于类卸载的条件非常严格(该类所有的实例都已被回收,加载该类的ClassLoader已经被回收等),导致永久代的内存经常只增不减,引发内存泄漏。 - 合并 HotSpot 与 JRockit 虚拟机:
Oracle 收购了 BEA 公司(拥有 JRockit JVM),打算将 HotSpot 和 JRockit 的优秀特性合并。JRockit 中并没有永久代的概念,为了统一架构,Oracle 决定废弃永久代。
3. JDK 7 到 JDK 8 的演变(重要分水岭)
为了平滑过渡,永久代的移除是分步骤进行的:
- JDK 6 及以前:永久代完整存在,包含上述所有内容。
- JDK 7:开始“拆除”永久代。
- 字符串常量池被从永久代移到了Java 堆中。
- 符号引用被移到了 Native Memory(本地内存)。
- 原因:字符串常量池在堆中更容易被垃圾回收,且与 Java 对象的生命周期管理更一致。
- JDK 8:彻底废除永久代。
- 将类元数据、运行时常量池等剩余内容全部移到了元空间中。
4. 永久代 vs 元空间
这是面试中最常被问到的对比:
| 特性 | 永久代 (JDK 7及以前) | 元空间 (JDK 8及以后) |
|---|---|---|
| 所在位置 | JVM 进程内存中 | 本地内存 |
| 默认大小 | 较小,受限于-XX:MaxPermSize | 默认无上限(仅受限于物理内存) |
| 内存溢出风险 | 极高 (PermGen space) | 较低,但可能耗尽物理内存导致被OS杀死 |
| GC 触发 | 频繁触发 Full GC | 元空间垃圾回收与堆解耦,条件更宽松 |
| 调优参数 | -XX:PermSize/-XX:MaxPermSize | -XX:MetaspaceSize/-XX:MaxMetaspaceSize |
5. 面试常考考点总结
- 方法区与永久代的关系?
- 方法区是 JVM 规范中定义的一个逻辑区域。
- 永久代是 JDK 7 以前 HotSpot JVM 对方法区的实现。
- 元空间是 JDK 8 及以后 HotSpot JVM 对方法区的实现。
- 字符串常量池在哪里?
- JDK 6:永久代。
- JDK 7 及以后:Java 堆中。
- 为什么用元空间替换永久代?
- 因为永久代大小固定,容易 OOM;而元空间使用本地内存,大小随需增长,极大地降低了由于加载过多类导致的
OOM问题。
- 因为永久代大小固定,容易 OOM;而元空间使用本地内存,大小随需增长,极大地降低了由于加载过多类导致的
注:如果你在运行 JDK 8 或更高版本的代码时,在启动参数中加上了-XX:MaxPermSize=512m,JVM 会直接报错并拒绝启动,提示该参数已被忽略或废弃。
