Java笔记 · · By/蜜汁炒酸奶

重学Java-String对象

前言

随着看的东西多了,越想把相关的知识点能串起来,所以有了这个重学系列,今后不定期更新。

String对象已经不是第一次写,在之前关于 String 的文章中也涉及过。随着时间的增长,再回头看之前的部分结论,越发感觉之前的文章存在一些问题,其中最关键问题是没能真正理解 运行时常量池与字符串池的关系,故有了本篇更精简版。

之前的文章可见下列,关于产生几个对象的部分可以对比看下之前的论证中的问题:

1. String 赋值方式

String s1 = new String("ab"); String s2 = "ab"; String s3 = "a"+"b"; String s4 = "a", s5 = "b", s6 = s4 + s5; String s7 = new String("a") + new String("b");

在上面每行都独立运行互不干扰的情况下,针对字符串对象创建,简单来说存在如下规则:

  • s1 产生两个对象:一个由字面量"ab"创建的字符串池对象,一个new创建的对象。
  • S2 产生一个对象:由字面量"ab"创建的字符串池对象
  • s3 在编译期间被优化为"ab",故等价于s2,此处不再讨论s3的情况。
  • 字符串被编译器优化
  • s6 和 s7 类似,内部都是通过 StringBuilder 拼接字符串,最终通过StringBuilder.toString:()方法生成一个 “ab” 对象,在调用 intern()之前,不会生成相应的字符串池对象。
  • s6 和 s7 不同之处在于,s7会多出一个"a"对象和一个"b"对象。原因参考 s1 和 s2。

2. Class字节码常量池

通过 javap -v String01.class 我们可以看到class文件的字节码信息,这里面常量池(Constant pool),简单来说保存的是该类的符号信息,其中:

  • CONSTANT_String_info 的结构用于标识这是个String对象,其保存了一个索引,该索引指向 CONSTANT_Utf8_info 结构。
  • CONSTANT_Utf8_info 结构中有由 Unicode code points 序列组成的字符串常量。
  • 从而 CONSTANT_String_info 结构给出了由 Unicode code points 序列组成的字符串常量。

相关英文原版参考【附录6.2 CLASS 常量池】
Class字节码常量池局部

3. 运行时常量池

Class字节码常量池中的信息会在类加载过程中进入运行时常量池,这个常量池更像是一个存储全局的类的符号信息的清单池,用于JVM运行过程中查询执行所需类、方法等相关信息并将其入栈。

相关英文原版参考【附录6.3 关于 JVM 从运行时常量池获取字符串】

3.1 部分关于String的Java语言规定

  1. 相同的字符串常量(即包含同一份Unicode code points 序列的常量)必须指向同一个String实例。
  2. 如果在任意字符串上调用 String.intern() 方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同

这意味着 ab永远与调用 intern 方法返回的结果所指向的类实例是同一个对象。

3.2 JVM查找创建字符串过程

为了得到字符串字面量(即常量),JVM需要检查 CONSTANT_String_info 结构中的 Unicode code points 序列:

  • 如果某String实例所包含的 Unicode code points 序列与CONSTANT_String_info 结构中的相同,而之前又曾在该实例上调用过String.intern()方法,那么此次字符串常量获取的结果将是一个指向相同String实例的引用,也就是说获取结果相当于该String实例。
  • 否则,会创建一个新的String实例,其中包含由 CONSTANT_String_info 结构给出的 Unicode code points 序列,字符串常量获取的结果是指向新的String实例的引用。最后,新String实例的intern方法被JVM自动调用

3.3 ldc 指令

ldc 指令是一个从运行时常量池提取数据并压入操作数栈的操作。关于字符串的操作部分如下:如果运行时常量池成员是一个代表字面量的String类的引用,那么这个实例的引用value将入栈到操作数栈中。简单来说就是将String对象的引用入栈。

实际上,HotSpot虚拟机在执行 ldc指令的时会执行上面JVM查找与创建String 对象的过程,并将引用保存在 字符串池 中。

相关英文原版参考【附录6.4 关于 ldc 指令】

4. 字符串池(String Pool)

字符串池目的是为了缓存字符串常量,实际上不是JVM规范中的,而是 HotSpot 虚拟机自己实现的产物:

  • Java调用 intern() 实际上是调用 c++ 实现的 StringTable 的 intern() 方法。
  • StringTable 实际上可以看做是一个 HashTable。
  • 字符串池这个 HashTable 保存的本质上是 reference。
  • 可以说是 HotSpot虚拟机针对 JVM规范中定义的 intern方法以及通过字符串常量缓存实现上面JVM查找和创建String对象过程的具体实现方案。
  • 基于上面的结论,字符串池给我的感觉是 由 HotSpot 实现的,运行时常量池的一部分,它的主要作用是用来实现字符串常量的存储操作的部分。

5. 实例

5.1 String02

public class String02 { public static void main(String[] args) { String s1 = new String("ab"); // ① String s3 = s1.intern(); // ② String s2 = "ab"; // ③ System.out.println(s1==s2); // false System.out.println(s1==s3); // false System.out.println(s2==s3); // true } }

直接运行将会得到 false false true 的结果,无论 ①、②、③行如何互换,结果不变。这也可以侧面证明上面提到的 关于String的Java语言规定的第二条。

5.2 String03

public class String03 { public static void main(String[] args) { String s1 = new String("a") + new String("b"); // ① String s3 = s1.intern(); // ② String s2 = "ab"; // ③ System.out.println(s1==s2); // true System.out.println(s1==s3); // true System.out.println(s2==s3); // true } }

直接运行时:

  • JDK 1.6时 会得到 false false true 的结果,
  • JDK 1.7及之后将会得到 true true true 的结果
  • 当 行② 与 行 ③ 置换后,将得到 false false true的结果。

原因:

  • 1.6时 intern() 会先在字符串池中查找 equal() 相等的字符串,假如存在就返回该字符串在字符串池中的引用。如果不存在,虚拟机会在永久代上创建一个新的实例,并将其引用保存到 StringTable 中。
    Jdk1.6中Str
  • 1.7及之后,字符串池不再在永久代,而是移入堆区。调用intern()时,如果字符串存在则和1.6时一样,不存在时,不再直接新建对象,而是先查找并引用堆中已有的实例,从而更方便地利用堆中的对象。
    Jdk1.7以后String对象分布情况
  • 当 行② 与 行 ③ 置换后,s2 执行 ldc期间会自动调用 intern(),从而将 s2 的引用保存在字符串池(至于为何是s2不是s1,参考上面提到的Java规范第2条),之后 s3 得到的指向的便是s2的引用。
    Jdk1.7以后交换执行步骤后String对象分布情况

6. 附录

6.1 方法区(Method Area)、永久代(PermGen Space)、元空间(Metaspace)

  • 方法区 是 JVM的规范,属于JVM内存模型中的区域。 是JVM 所有线程共享的、用于存储类的信息、常量池、方法数据、方法代码等。
  • 永久代 是 HotSpot在JDK7及之前针对 方法区的具体实现,jdk8中移除。
    • 从1.7开始移除工作,符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap。
    • 设置参数 -XX:PermSize=N -XX:MaxPermSize=N
  • 元空间 是 HotSpot在jdk8及之后的具体实现。元空间并不在虚拟机中,而是使用本地内存
    • 存储类的元数据
    • 其参数设置是 -XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N

6.2 CLASS 常量池

Java Virtual Machine instructions do not rely on the run-time layout of classes, interfaces, class instances, or arrays. Instead, instructions refer to symbolic information in the constant_pool table.

All constant_pool table entries have the following general format:

   cp_info {
       u1 tag;
       u1 info[];
   }

Each item in the constant_pool table must begin with a 1-byte tag indicating the kind of cp_info entry. The contents of the info array vary with the value of tag. The valid tags and their values are listed in Table 4.3. Each tag byte must be followed by two or more bytes giving information about the specific constant. The format of the additional information varies with the tag value.

截取自se8-jvms-4.4

The CONSTANT_String_info Structure

The CONSTANT_String_info structure is used to represent constant objects of the type String:

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

The items of the CONSTANT_String_info structure are as follows:

tag : The tag item of the CONSTANT_String_info structure has the value CONSTANT_String (8).

string_index : The value of the string_index item must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_Utf8_info (§4.4.7) structure representing the sequence of Unicode code points to which the String object is to be initialized.

截取自se8-jvms-4.4.3

The CONSTANT_Utf8_info Structure

The CONSTANT_Utf8_info structure is used to represent constant string values:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

The items of the CONSTANT_Utf8_info structure are the following:

tag: The tag item of the CONSTANT_Utf8_info structure has the value CONSTANT_Utf8 (1).

length : The value of the length item gives the number of bytes in the bytes array (not the length of the resulting string). The strings in the CONSTANT_Utf8_info structure are not null-terminated.

bytes[] : The bytes array contains the bytes of the string. No byte may have the value (byte)0 or lie in the range (byte)0xf0 - (byte)0xff.

String content is encoded in modified UTF-8. Modified UTF-8 strings are encoded so that code point sequences that contain only non-null ASCII characters can be represented using only 1 byte per code point, but all code points in the Unicode codespace can be represented.

截取自se8-jvms-4.4.7

6.3 关于 JVM 从运行时常量池获取字符串

A string literal is a reference to an instance of class String, and is derived from a CONSTANT_String_info structure (§4.4.3) in the binary representation of a class or interface. The CONSTANT_String_info structure gives the sequence of Unicode code points constituting the string literal.

The Java programming language requires that identical string literals (that is, literals that contain the same sequence of code points) must refer to the same instance of class String (JLS §3.10.5). In addition, if the method String.intern is called on any string, the result is a reference to the same class instance that would be returned if that string appeared as a literal. Thus, the following expression must have the value true:

("a" + "b" + "c").intern() == "abc"

To derive a string literal, the Java Virtual Machine examines the sequence of code points given by the CONSTANT_String_info structure.

  • If the method String.intern has previously been called on an instance of class String containing a sequence of Unicode code points identical to that given by the CONSTANT_String_info structure, then the result of string literal derivation is a reference to that same instance of class String.

  • Otherwise, a new instance of class String is created containing the sequence of Unicode code points given by the CONSTANT_String_info structure; a reference to that class instance is the result of string literal derivation. Finally, the intern method of the new String instance is invoked.

截取自se8-jvms-5.1

6.4 关于 ldc 指令

The index is an unsigned byte that must be a valid index into the run-time constant pool of the current class (§2.6). The run-time constant pool entry at index either must be a run-time constant of type int or float, or a reference to a string literal, or a symbolic reference to a class, method type, or method handle (§5.1).

Otherwise, if the run-time constant pool entry is a reference to an instance of class String representing a string literal (§5.1), then a reference to that instance, value, is pushed onto the operand stack.

截取自se8-jvms-6.5.ldc

评论已关闭

example
C
蜜汁炒酸奶

当前处于试运行期间,可能存在不稳定情况,敬请见谅。

欢迎点击此处反馈访问过程中出现的问题