【Java】字符串常量池深度解析:从面试题到内存原理

一、开篇灵魂拷问:new String("云扬") 到底创建了几个对象?

相信大家面试时都遇到过这个经典问题 ——一行 new String("云扬") 会创建几个对象?答案是 2 个! 先上代码实例,再拆解原理:

// 关键代码示例 1:new String 创建对象分析
public class StringPoolDemo {
    public static void main(String[] args) {
        // 这行代码触发两个对象创建
        String s = new String("云扬");
    }
}

内存模型拆解(结合 JVM 内存结构):

  1. 字符串常量池中的对象:JVM 先检查常量池是否存在 “云扬”,不存在则先在常量池创建一个字符串对象;
  2. 堆内存中的对象new String() 关键字强制在堆内存中创建一个新的字符串对象,内容与常量池中的一致;
  3. 栈上的引用:变量 s 存储在栈中,指向堆内存中的字符串对象。

补充:如果常量池已存在 “云扬”,则仅在堆中创建 1 个对象,但题目默认 “首次创建” 场景,故答案为 2 个。

二、字符串常量池的核心作用:复用 + 省内存

为什么 Java 要设计字符串常量池?因为字符串是开发中最常用的数据类型,复用已有对象可减少内存开销和 GC 压力。对比两种创建方式的差异:

// 关键代码示例 2:两种创建方式对比
public class StringCreateDemo {
    public static void main(String[] args) {
        // 方式 1:双引号直接创建(优先复用常量池对象)
        String s1 = "三妹";
        String s2 = "三妹";
        System.out.println(s1 == s2); // true(指向同一个常量池对象)
        
        // 方式 2:new 关键字创建(强制堆内存新建)
        String s3 = new String("云扬");
        String s4 = new String("云扬");
        System.out.println(s3 == s4); // false(堆中两个不同对象)
    }
}

核心结论:

  • 双引号创建:优先查询常量池,存在则直接返回引用,不存在则创建后返回(仅 1 个对象);
  • new 关键字创建:无论常量池是否存在,都会在堆中新建对象(最少 1 个,最多 2 个)。

三、字符串常量池的内存位置演变(JDK 版本差异)

JDK 版本常量池位置核心说明
JDK 6 及以前永久代(PermGen)永久代是堆的子区域,大小固定,易触发 OOM
JDK 7堆内存(Heap)解决永久代空间不足问题,动态扩容
JDK 8 及以后元空间(Metaspace)元空间使用本地内存,不受 JVM 堆大小限制,彻底解决 OOM

关键补充:永久代 vs 元空间

很多同学会混淆这三个概念,用通俗的比喻解释:

  • 方法区:JVM 规范中的 “接口”,定义了静态数据的存储规范;
  • 永久代:HotSpot 虚拟机对方法区的 “实现类”(JDK 8 前),存于堆中;
  • 元空间:JDK 8 后替代永久代的 “实现类”,存于本地内存,动态扩容。

四、实战避坑:字符串常量池的常见误区

误区 1:String.intern() 方法的作用

// 关键代码示例 3:intern() 方法实战
public class InternDemo {
    public static void main(String[] args) {
         // 字面量"云扬"在类加载时已入池,new String()在堆中创建新对象
        String s1 = new String("云扬");
        
        // 池中已有"云扬"(来自字面量),intern()返回池中引用(字面量对象)
        String s2 = s1.intern();
        
        // 直接使用字面量,返回池中同一引用
        String s3 = "云扬";
        
        // s1是堆中新对象,s2是池中字面量对象,二者不同
        System.out.println(s1 == s2); // false
        
        // s2和s3都指向池中同一对象
        System.out.println(s2 == s3); // true
    }
}

注意:JDK 7 后 intern() 不再复制对象到常量池,而是存储堆对象的引用,进一步节省内存。

误区 2:字符串拼接的常量池行为

// 关键代码示例 4:字符串拼接分析
public class StringConcatDemo {
    public static void main(String[] args) {
        String a = "云";
        String b = "扬";
        
        String c = "云扬";
        String d = a + b; // 编译期无法确定,堆中创建新对象
        String e = "云" + "扬"; // 编译期优化为 "云扬",复用常量池对象
        
        System.out.println(c == d); // false
        System.out.println(c == e); // true
    }
}

五、总结:字符串常量池的核心价值

  1. 性能优化:复用字符串对象,减少内存分配和 GC 次数;
  2. 面试重点new String 创建对象数、常量池位置演变、intern() 方法是高频考点;
  3. 开发建议:优先使用双引号创建字符串,避免不必要的 new String,减少内存浪费。

你在开发中有没有踩过字符串常量池的坑?或者遇到过其他相关的面试题?欢迎在评论区交流~

Tags:

发表回复

Your email address will not be published. Required fields are marked *.

*
*