String&&全局字符串常量池

18 篇文章 0 订阅

定义

String类

java语言规范是这样描述的

Instances of class String represent sequences of Unicode code points.

A String object has a constant (unchanging) value.

String literals (§3.10.5) are references to instances of class String.

The string concatenation operator + (§15.18.1) implicitly creates a new String object when the result is not a constant expression (§15.28).

中文翻译

String类的实例表示Unicode码位序列

每个String对象都有常量值(不可修改)

字符串字面常量 (§3.10.5)是对String类的实例的引用

如果字符串连接操作符(§15.18.1)运算的结果不是编译时的常量表达式,那么该操作符会隐式地创建新的String对象


String类的声明如下

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence{
    ...
    }
字符串字面量常量

java语言规范是这样描述的

A string literal consists of zero or more characters enclosed in double quotes. Characters may be represented by escape sequences (§3.10.6) - one escape sequence for characters in the range U+0000 to U+FFFF, two escape sequences for the UTF-16 surrogate code units of characters in the range U+010000 to U+10FFFF.

A string literal is always of type String (§4.3.3).

A string literal is a reference to an instance of class String (§4.3.1, §4.3.3).

Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.28) - are “interned” so as to share unique instances, using the method String.intern.

字符串字面量常量是由双引号括起来的0个或者是多个字符构成的,这些字符可以用转义序列 (§3.10.6)来表示,其中对于在从U+0000到U+FFFF范围内的字符的UTF-16代理码元,则需使用两个转义字符序列表示

字符串字面量常量的类型总是String

字符串面常量是对是对String类的实例的引用

而且一个字符串字面常量总是引用String类的同一个实例,这个是因为字符串字面常量,或者更一般的情况,表示常量表达式的值的字符串,被通过使用 String.intern() 方法限定了,这样做事为了让他们可以共享唯一的实例.

在java语言规范关于创建新的类的实例的时候有关于字符串字面量的描述

如下:

A new class instance may be implicitly created in the following situations:

  • Loading of a class or interface that contains a String literal (§3.10.5) may create a new String object to represent that literal. (This might not occur if the same String has previously been interned (§3.10.5).)

新的类实例可以在下列的情况下隐式的创建:

加载包含String字面量的类或者是接口时,会创建新的String对象,用来表示该字面常量(如果同一个String对象之前已经被限定了,那么这里就不会再创建新的String对象了)

字符串连接操作符

java语言规范描述如下

If only one operand expression is of type String, then string conversion (§5.1.11) is performed on the other operand to produce a string at run time.

The result of string concatenation is a reference to a String object that is the concatenation of the two operand strings. The characters of the left-hand operand precede the characters of the right-hand operand in the newly created string.

The String object is newly created (§12.5) unless the expression is a constant expression (§15.28).

An implementation may choose to perform conversion and concatenation in one step to avoid creating and then discarding an intermediate String object. To increase the performance of repeated string concatenation, a Java compiler may use the StringBuffer class or a similar technique to reduce the number of intermediate String objects that are created by evaluation of an expression.

For primitive types, an implementation may also optimize away the creation of a wrapper object by converting directly from a primitive type to a string.

如果只有一个操作数表达式是String类型,那么就会在另一个操作数上执行字符串转换以在运行时产生字符串

字符串连接操作符的结果是对String对象的引用,该对象是两个操作数字符串的连接,在新创建的字符串中,左操作数的字符在右操作数的字符的前面,

该String对象是新创建的 (§12.5),除非该表达式是编译时常量表达式 (§15.28).

java语言的实现可以选择在一个步骤中执行转换和连接,以避免先创建又丢弃中间的String对象,为了提高重复的字符串连接操作的可能性,java编译器可以使用Stringbuffer类 或类似的技术来减少计算表达式时所创建的中间String对象的数量,

对于简单类型,java语言的实现还可以通过直接将简单类型转换为字符串而优化掉包装器对象的创建

字符串常量池

在Java语言的规范和Jvm规范中其实是没有明确的阐述字符串常量池的,但是字符串常量池却是必须的 因为java语言规范规定 相同的字符串字面量共享唯一的String实例, 而这个是这个过程是通过String.intern() 方法实现的,在该方法中描述中

* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java&trade; Language Specification</cite>.
* public native String intern(){
.....
}

也说明了是通过一个被String类私自维护的全局共享的字符串常量池来实现的,

String.intern() 被调用时,如果常量池中已经有了一个string对象和当前对象相同则返回常量池中的string对象引用,如果没有则在堆中新创建一个String对象,并在字符串常量池中驻留其引用

String.intern() 是一个native方法 底层是由C++维护的一个StringTable(类似HashTable)来实现对象的存储的,换句话说字符串常量池是一个由C++维护的一个StringTable来实现的,

实践

字符串字面量

String st = "java";

在上述java定义中提到 字符串面常量是对String类的实例的引用,且对应同一个String实例

但是代码 String st = "java";并没有创建对象,st对应的是哪个String实例对象,并且是在什么时候创建的呢?

在jvm虚拟机规范5.1节中关于运行时常量池有这么一段描述(中文版翻译):


java虚拟机为每个类型都维护着一个常量池(2.5.5).该常量池是Java虚拟机中的运行时数据结构,像传统编程语言实现中的符号表一样有很多的用途.

当类或者是接口创建时(5.3),它的二进制表示中的常量池(4.4)被用来构造运行时常量池.运行时常量池中的所有引用都是符号引用.这些符号都是按照如下方式从类或接口的二进制表示中得出的:

....

字符串常量是指向String类实例的引用,它来自于类或接口二进制表示中的CONSTANT_String_info结构(见4.4.3小节).CONSTANT_String_info结构给出了由Unicode码点(code point)序列所组成的字符串常量

Java语言规定,相同的字符串常量(也就是包含同一份码点序列的常量)必须指向同一个String类实例.此外,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同.因此,下列表达式的值必定是true:

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

为了得到字符常量,Java虚拟机需要检查CONSTANT_String_info结构中的码点序列.

  • 如果某String实例所包含的Unicode码点序列与CONSTANT_String_info结构所给出的序列相同,而之前又曾在该实例上面调用过String.intern方法,那么此次字符串常量获取的结果将是一个指向相同String实例的引用.
  • 否者,会创建一个新的String实例,其中包含了CONSTANT_String_info结构所给出的码点序列;字符常量获取的结果指向那个新的String实例的引用.最后,新的String实例的intern方法被java虚拟机自动调用.

....


我们梳理下Jvm针对字符串常量的处理流程

一、java代码编译成class文件时,会将编译期间获取的字面量符号引用存储到class文件的常量池中,字面量一般对应着final修饰的常量、文本字符串等,比如如下代码,我们可以查看字节码中是否有 文本字符串 “java”

public class Test {
    public void func1(){
        String st = "java";
    }
}

执行 javap -v
在这里插入图片描述

反编译结果显示 文本字符串"java" 在编译后存入到class文件的常量池中,

二、在类或者接口创建时 Jvm会使用class文件的的常量池构造运行时常量池, 存储在常量池中的文本字符串会转换成字符常量并指向一个String实例对象,而在获取字符常量的火过程中

  1. 首先判断字符串常量池中是否有相同的String对象(jvm规定该String拥有相同的码点序列且调用了intern方法),如果有则返回该对象引用,
  2. 否者自动创建一个拥有相同String对象,并将其引用驻留在常量池中(jvm自动调用intern方法).字符常量指向新创建的String对象.

这里我们得到在构建运行时常量池时 jvm会为常量池中的字符串常量 赋值引用,那么该引用是如何指向我们定义的如 String st = "java" 中的st变量的呢?

上面反编译的结果中 String st = "java" 对应着字节码:

0: ldc      #2         // String java
2: astore_1

ldc指令是将int、float或者是 String类型的常量值从常量池中推送到栈顶

astore_1指令是 将栈顶引用型数值存入第二个本地变量

上述字节码表示 在代码运行期间才会从常量池获取相同值的String对象引用复制给变量

String对象

前面我们对字符串字面量 进行了反编译,那么新创建的String对象是如何的呢

public class Test {
    public void func1(){
        String st = new String("java");
    }
}

反编译:
在这里插入图片描述

String st = new String("java");的字节码如下

0:new      #2         // class java/lang/String
3:dup
4:ldc      #3         // String java
6:invokespecial #4         // Method java/lang/String."<init>":(Ljava/lang/String;)V
9:astore_1
第一行 new 创建一个String对象并将其引用压入栈顶
第三行 ldc 将int、float或者是  String类型的常量值从常量池中推送到栈顶

在新创建一个String对象st时,JVM首先会在堆中创建一个对象,其次在类加载期间 创建运行期常量池的时候 创建和其字面量相同的String对象,并在程序运行时 将引用赋值给st对象

总结
  1. 字符串常量池 是由native方法C++语言中StringTable类维护的一个类似HashTable表,其中存储的是对象引用
  2. 所有在编译期可知的字符串字面量,JVM都会在运行时常量池创建时,自动创建含有和字符串字面量相同Unicode码位序列的String对象并调用其intern方法,将其引用在字符串常量池中驻留.

注意(Hospot): 方法区变更

jdk1.6及之前有永久代(permanent generation),静态变量存放在永久代上
jdk1.7有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移至堆中
jdk1.8及之后无永久代,类型信息、域信息(字段)、方法信息、常量保持在本地内存的元空间,但字符串常量池、静态变量仍在堆中

实践 Jdk1.8

操作符 +

String st1 = 1 + 1 + "2";
String st2 = "2" + 1 + 1;

结果:
st1=22
st2=211

操作符 +是左侧运算符,

常量池:
例1

 String st1 = "java";
 String st2 = "java";
 System.out.println("st1==st2:" + (st1 == st2));
 结果:true
 字符串字面量对应同一个String对象

例2

 String st1 = "java";
 String st2 = new String("java");
 System.out.println("st1==st2:" + (st1 == st2));
 结果:false
 st1对应jvm自动创建的String对象
 st2内部持有jvm自动创建的String对象引用
 比如:
        String st1 = new String("java");
        String st2 = "java";
        String st3 =st1.intern();
        System.out.println("st1==st2:" + (st3 == st2));
        //结果为true

例3

 String st1 = "java";
 String st3 =st1.intern();
 System.out.println("st1==st3:" + (st1 == st3));
 结果:true

例4

 String st1 = new String("java");
 String st2 = st1.intern();
 System.out.println("st1==st2:" + (st1 == st2));
 结果:false

例5

 String st1 = new String("ja") + new String("va");
 st1.intern();
 String st2 = "java";
 System.out.println("st1==st2:" + (st1 == st2));
 结果:true
 
 String对象之间进行+操作符运算,会被编辑器优化为 StringBuilder的append运算,最终调用toString()方法,
 StringBuilder的toString()方法内部实现为:
 return new String(this.value, 0, this.count)
 即是重新根据最终的字符串创建了一个String对象.
   
  前面我们提到了JVM在创建运行时常量池时,会根据字符串字面量创建String对象并调用intern方法,此时按照我们之前的分析 在编译器期间可知的     字面量有 "ja" "va" "java",在创建运行时常量池会自动创建是三个String对象并在字符串常量池中驻留其引用,那么此处st1为新创建的含有java字符序列的String对象,  
  那么在st1.intern()执行时 此时字符串常量池中应该是有个 "java" String对象的,并且st1.intern()的返回值应该和st1不相等,
  String st2 = "java"; st2为字面量 st2对应的应该也是字符串常量池中的String对象,也就是说st2应该不等于st1.但是此处结果却显示两者相等!!!  那么肯定是有些地方是我们没有考虑到的 
  翻了很多资料最后再知乎上得到这个答案 https://www.zhihu.com/question/55994121/answer/147296098
其回答为::
() 在类加载阶段, JVM会在堆中创建对应这些 class 文件常量池中的字符串对象实例 ,并在字符串常量池中驻留其引用。具体在resolve阶段执行。这些常量全局共享。
() JVM规范里Class文件的常量池项的类型,有两种东西:
  1.CONSTANT_Utf8
  2.CONSTANT_String
  后者是String常量的类型,但它并不直接持有String常量的内容,而是只持有一个index,
  这个index所指定的另一个常量池项必须是一个CONSTANT_Utf8类型的常量,这里才真正持有字符串的内容。
  在HotSpot VM中,运行时常量池里,
  CONSTANT_Utf8 -> Symbol*(一个指针,指向一个Symbol类型的C++对象,内容是跟Class文件同样格式的UTF-8编码的字符串)        		
  CONSTANT_String -> java.lang.String(一个实际的Java对象的引用,C++类型是oop) 
  CONSTANT_Utf8会在类加载的过程中就全部创建出来,而CONSTANT_String则是lazy resolve的,
  例如说在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。那么在尚未resolve的时候,HotSpot VM把它的类型叫做JVM_CONSTANT_UnresolvedString,内容跟Class文件里一样只是一个index;等到resolve过后这个项的常量类型就会变成最终的JVM_CONSTANT_String,而内容则变成实际的那个oop。 
  看到这里想必也就明白了, 就HotSpot VM的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在StringTable中并没有相应的引用,在堆中也没有对应的对象产生)

总结:运行时常量池在获取字符串的String引用时,是采用懒解析的方式进行的 在没有执行ldc指令前是JVM_CONSTANT_UnresolvedString类型且指向是一个index ,该index指向的是运行时常量池中的CONSTANT_Utf8类型值,其内容和字符串常量的Unicode码位序列相同,当执行ldc指令时,JVM判断该CONSTANT_String是否被解析,如果没有则解析并将解析后的引用推到栈顶,否者直接返回其引用将其推到栈顶,

现在再次分析上面的代码,在代码编译期,首先生成"ja" "va" "java"的CONSTANT_Utf8类型值
 在代码运行期间
  第一行: String st1 = new String("ja") + new String("va"); 生成新的String对象 其值为 "java" 此时在字符串常量池中没有"java",
  第二行: st1.intern(); 调用intern方法 因为此时字符串常量池中没有"java",所以直接将st1引用驻留在字符串常量池中
  第三行: String st2 = "java"; 声明字面量"java" 执行dlc 指令,从常量池中查找 "java" 对象即是st1 将其赋值给st2 
    所以 st1==st2

例6

 String st1 = new String("ja") + new String("va");
 String st2 = "java";
 st1.intern();
 System.out.println("st1==st2:" + (st1 == st2));
 结果:false
 
 在上面总结了 JVM创建运行时常量池中的字符串常量的时机,以及dlc运行的基本判断
  所以上述代码分析:
第一行  String st1 = new String("ja") + new String("va"); 生成新的String对象,其值为"java"
第二行  String st2 = "java"; 声明字面量 执行dlc指令 此时字符串常量池中没有"java"对象引用,且运行时常量池中的 "java" 还未解析,则此时解析,创建新的String对象(其值为"java")并调用intern方法,并将该对象引用赋值给 st2
第三行   st1.intern();  st1调用intern(),因为此时字符串常量池中有"java"对象,所以将该对象引用(即是st2)直接返回
  
  所以st1不等于st2

关于创建对象

1,String st2 = "java"; 涉及了几个对象
答:涉及到1个对象,该对象是JVM自动创建的
2, String st1 = new String("java");涉及了几个对象
答:涉及到2个对象,第一个是用户声明的在堆上创建的String对象st1,第二个是JVM自动创建的含有"java"码点序列的String对象
3,String st1 = new String("ja") + new String("va");涉及了几个对象
答:52个用户自己创建的匿名String对象,2个JVM自动创建的分别包含 "ja" "va"码点序列的String对象并在字符串常量池中驻留了其引用,1个是编辑器优化代码使用StringBuilder调用tostring方法创建的包含"java"码点序列的String对象

参考:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值