高频八股 · JVM(7):方法区、元空间与字符串常量池

高频八股 · JVM(7):方法区、元空间与字符串常量池

写在前面

前几期讲的是堆 和对象。这一期讲两块"配角",但它们恰恰是面试最爱挖的坑:

  1. 方法区、永久代、元空间------这三个词经常被混着说,其实不是一回事;
  2. 字符串常量池 ------String 是 Java 里最特殊的对象,它的行为跟"池在哪"强相关。

先把结论放这儿:

方法区是"规范",永久代和元空间是"两代实现";而字符串常量池在 JDK 7 之后,从方法区搬到了堆里**------这一搬,改变了很多经典题目的答案。

一、方法区 / 永久代 / 元空间:三者什么关系

名词 是什么 位置
方法区 JVM 规范里的逻辑概念 规范没规定
永久代(PermGen) JDK 7 及以前的实现 堆内 (受 -XX:MaxPermSize 限制)
元空间(Metaspace) JDK 8 及以后的实现 本地内存 (受 -XX:MaxMetaspaceSize 限制)

关系一句话 :方法区是接口,永久代和元空间是两个不同的实现类。

1.1 为什么 JDK 8 要换成元空间?

四个原因:

  1. 永久代大小难调 ------设小了 PermGen space OOM,设大了浪费堆;
  2. 和对象抢堆空间------类加载多了会把堆挤爆;
  3. 永久代 GC 效率低------回收条件苛刻,垃圾容易堆积;
  4. 和 JRockit 合并------JRockit 没有永久代,为了统一实现。

换成元空间后 :类元信息搬到本地内存 ,PermGen space 这个经典 OOM 就消失了。

但别误会------元空间不是无限大 。如果不设 MaxMetaspaceSize,大量动态代理 / 热部署同样会把本地内存吃光,报 Metaspace OOM。

二、方法区里到底存了什么?

  • 类的元信息(类结构、字段、方法、接口、父类);
  • 运行时常量池;
  • 静态变量(JDK 7 起移到了堆);
  • JIT 编译后的代码缓存。

注意两个"搬家":

  • JDK 7 :字符串常量池从永久代 → 堆 ;静态变量也从永久代 → 堆;
  • JDK 8:永久代整体被元空间取代。

三、三个"常量池",别再搞混

这是最容易乱的地方。其实有三个不同的东西:

名称 在哪 是什么
class 文件常量池 .class 文件里 编译期生成的常量表(字面量 + 符号引用)
运行时常量池 方法区 / 元空间 class 常量池加载后 的版本,可动态添加(如 intern())
字符串常量池(StringTable) JDK 7 起在堆 专门存字符串字面量的哈希表

记法:

  • class 常量池是"文件里的";
  • 运行时常量池是"加载后的";
  • 字符串常量池是"专门管字符串的"。

四、String 为什么不可变?

先看它的定义(JDK 9 之后):

java 复制代码
public final class String {
    private final byte[] value;   // JDK 9 起从 char[] 改成 byte[]
    private final byte coder;     // 编码标识:LATIN1 / UTF16
    private int hash;             // 缓存哈希值
}

三层"不可变"保障:

  1. 类是 final,不能被继承改写;
  2. 内部数组是 private final,外部拿不到引用;
  3. 所有"修改"方法(substring、concat、replace)都返回新对象。

JDK 9 的改动叫紧凑字符串(Compact Strings):Latin-1 字符只占 1 字节,中文等占 2 字节,整体能省 30%~50% 内存。

4.1 不可变带来四个好处

好处 说明
线程安全 状态不可改,天然安全
哈希可缓存 hash 只算一次,适合做 HashMap 的 key
常量池可行 多个引用共享同一个对象
安全 类名、URL、密码传参不会被中途篡改

五、字面量 vs new:到底创建几个对象

5.1 字面量:先查池

java 复制代码
String a = "abc";

流程:先在字符串常量池里找 "abc"。

  • 有 → 直接返回那个引用;
  • 没有 → 在池里创建,再返回。

所以:

java 复制代码
String a = "abc";
String b = "abc";
System.out.println(a == b);   // true(同一个池对象)

5.2 new:绕开池,直接在堆里建

java 复制代码
String c = new String("abc");
System.out.println(a == c);   // false(一个在池,一个在堆)

5.3 new String("abc") 到底几个对象?

答案:1 个或 2 个。

  • 2 个 :池里原本没有 "abc" → 池里创建 1 个 + 堆里创建 1 个;
  • 1 个 :池里已经有 "abc" → 只创建堆里那 1 个。

5.4 两个"坑"例子

java 复制代码
String a = "a" + "b";    // 编译期直接优化成 "ab"
String b = "ab";
System.out.println(a == b);   // true
java 复制代码
String x = "a";
String y = "b";
String a = x + y;        // 运行时用 StringBuilder 拼接
String b = "ab";
System.out.println(a == b);   // false(a 是堆里的新对象)

区别在于 :编译期常量折叠 vs 运行时拼接。

六、intern():JDK 6 和 JDK 7 的答案不一样

intern() 的作用 :把字符串放进常量池,并返回池里那个引用。

关键差异在于"池在哪":

版本 字符串常量池位置 intern() 的行为
JDK 6 方法区(永久代) 池里没有 → 复制一份到永久代
JDK 7+ 堆 池里没有 → 直接把堆里的引用存进池(不复制)

经典题目:

java 复制代码
String s = new String("a") + new String("b");  // "ab" 在堆里
s.intern();
String s2 = "ab";
System.out.println(s == s2);

答案:

  • JDK 6:false ------ intern() 把 "ab" 复制 到永久代,s 还在堆里,两个不同对象;
  • JDK 7+:true ------ intern() 把 s 的引用 放进池,s2 取到的就是 s 本身。

这题能答出"因为池从永久代搬到了堆",基本就赢了。

七、StringTable:只冲突,不溢出

这是你手册里一个很好的点,值得展开。

StringTable 本质是一个哈希表 (底层是数组 + 链表,跟 HashMap 类似),默认容量约 60013 (-XX:StringTableSize 可调)。

当字符串非常多时,会发生什么?

  • 哈希冲突变多 → 链表变长 → intern() / 字面量查找变慢;
  • 但不会"溢出"------因为它只是个表,链表可以无限延长(理论上)。

真正会 OOM 的是什么?

是字符串对象本身 。它们住在堆里,字符串太多、太大,报的是:

复制代码
java.lang.OutOfMemoryError: Java heap space

所以结论是:StringTable 只影响性能(查找变慢),字符串本身才影响内存(OOM)。

调优手段 :-XX:StringTableSize=1000003(调大桶数,减少冲突)。

八、常量池会被回收吗?

会。

字符串常量池里的字符串,如果没有任何引用 指向它,在 Full GC 时是可以被回收的。

这也是为什么"用 intern() 缓存大量字符串"要小心------它不是"永久存在"的。

九、高频面试题速答

Q1:方法区、永久代、元空间什么关系?

方法区是 JVM 规范里的概念;永久代是 JDK 7 及以前的实现(堆内);元空间是 JDK 8+ 的实现(本地内存)。

Q2:JDK 8 为什么用元空间替换永久代?

永久代大小难调、和堆抢空间、GC 效率低,且需与 JRockit 统一实现。

Q3:String s = new String("a") 创建几个对象?

1 个或 2 个:池里没有 "a" 就是 2 个(池 1 + 堆 1);池里已有就是 1 个(堆 1)。

Q4:intern() 在 JDK 6 和 JDK 7 有什么不同?

JDK 6 池在永久代,intern() 会复制 字符串过去;JDK 7+ 池在堆,intern() 直接存引用 ,不复制。所以 s == s2 的结果从 false 变成 true。

Q5:三个常量池怎么区分?

class 常量池在 .class 文件里;运行时常量池在方法区(元空间),是加载后的版本;字符串常量池在堆(JDK 7+),专门管字符串。

Q6:字符串常量池会 OOM 吗?

表本身只会有哈希冲突、查找变慢;真正 OOM 的是堆里的字符串对象。

十、一句话总结

方法区是规范,永久代和元空间是两代实现;字符串常量池在 JDK 7 搬进了堆,所以 intern() 从"复制"变成了"存引用"------这一搬,改写了一批经典面试题的答案。


(本系列持续更新中,共 10 期,敬请关注~)

相关推荐
LazyHunter91 小时前
Spring Boot 3 微服务实践指南
java·spring boot
又过二十1 小时前
Qwen-Image2.1游戏素材工坊:让角色真的走起来(三)
ai·aigc·comfyui·game·qwen-image
zzu123zsw1 小时前
从 SPI 契约到独立 ClassLoader 隔离:Spring Boot 插件热插拔架构设计与内存泄露防御
java·spring boot·后端·架构设计
文丑颜不良啊1 小时前
LeetCode1161~1180题目解析
java·leetcode
古法老登瞎折腾9 小时前
大数据智能运维实战(十一):运维 MCP 的工程化——权限、限流、审计与错误处理
java·大数据·运维·spring ai·mcp
智能运维指南10 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
dawnsky.liu10 小时前
在 RHEL 中使用 goose 实现远程主机管理
linux·运维·ai
在世修行10 小时前
智学文枢:变式题生成引擎
python·ai·pyside·题目生成
Industio_触觉智能11 小时前
把NPU检测直接嵌进采集终端|RK3576 AI采集终端全链路拆解教程-下
嵌入式硬件·ai·硬件架构·npu·ai视觉·rk3576·采集终端