写点什么

不再为 String 创建对象而烦恼!一文读懂底层原理

作者:储诚益
  • 2025-01-28
    安徽
  • 本文字数:3215 字

    阅读完需:约 11 分钟

Java 中 String 对象的创建过程涉及多个步骤,包括字符串字面量的处理、new String() 的实例化、字符串常量池的管理等。以下是详细的分析,涵盖从 Java 代码到 JVM 内部的完整过程。

1. 字符串字面量的创建

字符串字面量(如 "hello")是 Java 中最常见的字符串创建方式。它的创建过程如下:

String s1 = "hello";
复制代码

编译阶段的实现

在编译阶段,编译器(如 javac)会进行以下操作:编译器会将字符串字面量(如 "hello")存储到类文件的常量池(Constant Pool)中;编译器会生成字节码指令,用于在运行时从常量池中加载字符串字面量。对于 String s = "hello";,编译器会生成类似以下的字节码:

ldc #2  // 从常量池中加载字符串 "hello"astore_1 // 将加载的字符串引用存储到局部变量表
复制代码

在编译后的类文件中,字符串字面量 "hello" 会被存储为常量池中的一个条目。例如:

Constant Pool:  #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V  #2 = String             #21            // hello  #3 = Class              #22            // java/lang/String  ...  #21 = Utf8               hello
复制代码
  • #2 是字符串常量池中的一个条目,指向 #21

  • #21 是一个 Utf8 条目,存储了字符串的实际内容("hello")。

运行阶段的实现

当类被加载到 JVM 时,JVM 会解析类文件的常量池,并将字符串字面量加载到 JVM 的字符串常量池(String Pool)中。

  • 解析常量池

JVM 会读取类文件中的常量池,找到所有的字符串字面量(如 "hello")。对于每个字符串字面量,JVM 会检查字符串常量池中是否已经存在相同内容的字符串。

  • 加载到字符串常量池

如果字符串常量池中不存在相同内容的字符串,JVM 会创建一个新的 String 对象,并将其添加到字符串常量池中。如果已经存在,则直接使用池中的字符串引用。

当代码执行到字符串字面量的引用时,JVM 会从字符串常量池中获取对应的字符串对象。

  • 字节码指令的执行

对于 ldc #2 指令,JVM 会从常量池中加载 #2 对应的字符串对象。如果字符串常量池中已经存在 "hello",则直接返回其引用。如果不存在,则创建新的 String 对象并添加到字符串常量池中。

2. new String() 的创建

使用 new String() 创建字符串对象时,会显式地在堆内存中创建一个新的对象。

String s = new String("hello");
复制代码

编译器的处理

在编译阶段,编译器(如 javac)会进行以下操作:

  • 处理字符串字面量:编译器会将字符串字面量 "hello" 存储到类文件的常量池(Constant Pool)中。常量池中的字符串字面量会在类加载时被加载到 JVM 的字符串常量池(String Pool)中。

  • 生成字节码指令:编译器会生成字节码指令,用于在运行时创建 String 对象。对于 String s = new String("hello");,编译器会生成类似以下的字节码:

ldc #2  // 从常量池中加载字符串 "hello"invokespecial #3  // 调用 String 类的构造方法astore_1 // 将创建的 String 对象引用存储到局部变量表
复制代码

在编译后的类文件中,字符串字面量 "hello" 会被存储为常量池中的一个条目。例如:

Constant Pool:  #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V  #2 = String             #21            // hello  #3 = Methodref          #5.#22         // java/lang/String."<init>":(Ljava/lang/String;)V  ...  #21 = Utf8               hello
复制代码

#2 是字符串常量池中的一个条目,指向 #21#21 是一个 Utf8 条目,存储了字符串的实际内容("hello")。#3 是 String 类的构造方法引用。

运行阶段的实现

当类被加载到 JVM 时,JVM 会解析类文件的常量池,并将字符串字面量加载到 JVM 的字符串常量池(String Pool)中。当代码执行到 new String() 时,JVM 会进行以下操作:

加载字符串字面量:执行 ldc #2 指令,从常量池中加载字符串字面量 "hello" 的引用。如果字符串常量池中已经存在 "hello",则直接返回其引用;如果不存在,则创建新的 String 对象并添加到字符串常量池中。

创建新的 String 对象:执行 invokespecial #3 指令,调用 String 类的构造方法。当 JVM 执行 new String() 时,会调用 java_lang_String::create_from_string() 方法来创建 String 对象。

// hotspot/src/share/vm/classfile/javaClasses.cpp
// 创建一个新的 String 对象,oop str是常量池中的对象oop java_lang_String::create_from_string(oop str, TRAPS) { // 获取字符串的内容和长度 jchar* chars = as_unicode_string(str); int len = length(str);
// 创建一个新的 String 对象 oop result = create_oop_from_unicode(chars, len, CHECK_NULL); return result;}
复制代码

create_oop_from_unicode() 方法用于创建一个新的 String 对象,并将其内容初始化为指定的字符数组。

// hotspot/src/share/vm/classfile/javaClasses.cpp
// 从 Unicode 字符数组创建 String 对象oop java_lang_String::create_oop_from_unicode(jchar* unicode, int length, TRAPS) { // 分配一个新的 String 对象 oop result = InstanceKlass::cast(SystemDictionary::String_klass())->allocate_instance(CHECK_NULL); // 初始化 String 对象的 value 字段 typeArrayOop value = oopFactory::new_charArray(length, CHECK_NULL); for (int i = 0; i < length; i++) { value->char_at_put(i, unicode[i]); } java_lang_String::set_value(result, value);
return result;}
复制代码
  • allocate_instance():从 JVM 的堆内存分配一个新的 String 对象。

  • new_charArray():创建一个新的字符数组(char[]),用于存储字符串内容。

  • set_value():将字符数组赋值给 String 对象的 value 字段。

3. 两种创建方式的特性

字符串字面量的创建和 new String() 的创建在 Java 中有一些相同点,但也有显著的差异。

相同点

  • 内容不可变性:两种方式创建的字符串对象都是不可变的,即一旦创建,字符串的内容就无法被更改。这是 Java 字符串设计的一个重要特性,有助于提升性能及安全性。

  • 数据类型相同:无论是通过字符串字面量创建还是通过 new String() 创建,最终得到的对象都是 String 类型

差异点

  • 内存分配不同:当使用字符串字面量创建字符串时,JVM 会首先检查字符串常量池中是否已经存在与该字面量内容相同的字符串对象。如果存在,则直接返回该对象的引用;如果不存在,则在堆中创建一个新的字符串对象,并将其引用放入字符串常量池中。每次使用 new String() 创建字符串时,无论字符串常量池中是否存在相同内容的字符串对象,都会在堆中创建一个新的字符串对象

  • 对象引用不同:如果两个变量使用了相同的字符串字面量赋值,那么这两个变量实际上引用的是同一个字符串对象。即使两个变量使用了相同的字符串内容来调用 new String(),它们也会引用不同的字符串对象

  • 性能开销不同:由于可能复用已有的字符串对象,因此在某些情况下可以减少内存开销和提高性能。new String():由于每次都会创建新的字符串对象,因此在内存开销上可能相对较大。

通过理解两者的相同点和差异点,可以更好地选择适合的字符串创建方式,优化内存使用和性能。

4. 代码案例

  • 字符串字面量

String s1 = "hello";  // 字符串常量池中创建 "hello"String s2 = "hello";  // 直接从字符串常量池中获取引用System.out.println(s1 == s2); // true,s1 和 s2 指向同一个对象
复制代码
  • new String()

String s3 = new String("hello");  // 在堆中创建一个新的对象String s4 = new String("hello");  // 在堆中创建另一个新的对象System.out.println(s3 == s4); // false,s3 和 s4 是不同的对象
复制代码


  • new String() 与 intern()

String s5 = new String("hello").intern();  // 将堆中的对象添加到字符串常量池String s6 = "hello";  // 直接从字符串常量池中获取引用System.out.println(s5 == s6); // true,s5 和 s6 指向同一个对象
复制代码


发布于: 刚刚阅读数: 5
用户头像

储诚益

关注

还未添加个人签名 2017-12-19 加入

还未添加个人简介

评论

发布
暂无评论
不再为String创建对象而烦恼!一文读懂底层原理_string_储诚益_InfoQ写作社区