欢迎光临
我们一直在努力

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

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

写在前面

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

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

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

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

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

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

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

    四个原因:

  • 永久代大小难调——设小了 PermGen space OOM,设大了浪费堆;
  • 和对象抢堆空间——类加载多了会把堆挤爆;
  • 永久代 GC 效率低——回收条件苛刻,垃圾容易堆积;
  • 和 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 之后):

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

    三层"不可变"保障:

  • 类是 final,不能被继承改写;
  • 内部数组是 private final,外部拿不到引用;
  • 所有"修改"方法(substring、concat、replace)都返回新对象。
  • JDK 9 的改动叫紧凑字符串(Compact Strings):Latin-1 字符只占 1 字节,中文等占 2 字节,整体能省 30%~50% 内存。

    4.1 不可变带来四个好处

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

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

    5.1 字面量:先查池

    String a = "abc";

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

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

    所以:

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

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

    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 两个"坑"例子

    String a = "a" + "b"; // 编译期直接优化成 "ab"
    String b = "ab";
    System.out.println(a == b); // true

    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+ 堆 池里没有 → 直接把堆里的引用存进池(不复制)

    经典题目:

    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 期,敬请关注~)

    赞(0)
    未经允许不得转载:171主机测评 » 高频八股 · JVM(7):方法区、元空间与字符串常量池
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址