JVM的常量池:String.intern()的理解以及字符串常量池解析

如果您想了解关于几个常量池之间的知识,请您看这篇JVM的常量池:什么是字符串常量池、运行时常量池、Class常量池

 

大致过程:

在该类的class常量池中会存放一些符号引用,在类加载之后,会将class常量池中存放的符号引用加载到内存中的运行时常量池中,然后经过验证,准备阶段之后,会在堆中生成驻留字符串的实例对象(也就是””括起来的),然后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,通过直接查询StringTable,保证运行时常量池里的引用值与字符串常量池中的引用值一致。

 

一、字符常量如何进入常量池

8种基本类型的常量池都是系统协调的但String类型比较特殊,之后的运行过程涉及到常量池内容添加和String对象的两种创建方式的关系:

(1)String str1 = "abcd"

采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"abcd"这个对象,如果常量池是空的会直接在字符串常量池中创建该双引号声明出来的 String 对象,然后将池中"abcd"这个对象的引用地址返回给对象的引用str1,如果存在,则不创建任何对象,直接将池中"abcd"这个对象的地址返回。

 

扫描二维码关注公众号,回复: 11639777 查看本文章

(2)String str1 = new String("abcd")

采用new关键字新建一个字符串对象时,JVM首先在字符串池中查找有没有"abcd"这个字符串对象,如果没有,则首先在字符串池中创建一个"abcd"字符串对象,然后再在堆中创建一个"abcd"字符串对象,然后将堆中这个"abcd"字符串对象的地址返回赋给str1引用,如果有,则不在常量池中再去创建"abcd"这个对象了,直接在堆中创建一个"abcd"字符串对象,然后将堆中的这个"abcd"对象的地址返回赋给引用。

 

(3)通过String.intern()

intern()方法设计的初衷,就是重用String对象,以节省内存消耗,String对象可以使用 String.intern()来动态的进入常量池,intern() 方法返回字符串对象的规范化表示形式,它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

 

二、对String.intern()的理解

 

String.intern() 是一个 Native 方法,以下是文档内容:

《java虚拟机规范 Java SE 8版》记录,如果某String实例所包含的Unicode码点序列与CONSTANT——String_info结构所给出的序列相同也就是字符连起来通过equals()比较是否相同,而之前又曾在该实例上面调用过String.intern方法,那么此次字符串常量获取的结果将是一个指向相同String实例的引用。

 

Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true. All literal strings and string-valued constant expressions are interned.

 

返回字符串对象的规范表示形式。字符串池最初是空的,由class常量池私下维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与此字符串对象相等的字符串,则返回池中的字符串。否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。因此,对于任意两个字符串s和t, s.intern() == t.intern()在且仅当s.equals(t)为真时为真。所有文本字符串和字符串值常量表达式都是内部连接的。

 

intern用来添加和返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后返回引用,在JDK1.6和1.7它的操作不同:

JDK1.6:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用,如果没有那么intern()方法会把首次遇到的字符串实例复制到永久代的常量池中,之后返回的是永久代常量池中字符串的引用,而之前创建的字符串实例依然在Java堆上,此时字符串即在常量池也在堆中。

JDK1.7:intern()还是会先去查询常量池中是否有已经存在相同内容的字符串,如果常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用,区别在于,如果在常量池找不到对应的字符串则不会再将字符串拷贝到常量池,而只是在常量池中生成一个指向堆中字符串对象的引用,并返回这个引用,此时被引用的字符串还在堆中原来的地方。

简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。

 

三、对不同情况下生成的字符串进行比较,探究生成的位置

 

举几个实例来说明一下:

public class HelloWorld {
    public static void main(String []args) {
        //在堆中会有一个”abc”实例,也就是字符串常量池里全局StringTable中存放着的”abc”
        String str1 = "abc"; 
        //这时会生成两个实例,一个是new出来的一个”def”的实例对象,并且StringTable中会存储一个”def”的引用值,
        String str2 = new String("def"); 
        //在解析str3时查找常量池发现里面有”abc”的全局驻留字符串引用,那么str3不会在常量池中申请新的空间,
        //而是直接把在常量池中已存在的字符串内存地址返回给s2,所以str3的引用地址与之前的那个已存在的相同
        String str3 = "abc"; 
        //如果常量池有就返回StringTable中的引用值,如果没有就将str2的引用值或字符串添加进去,根据不同的版本会有不同的操作
        String str4 = str2.intern(); 
        //在解析的时候就会去查找常量池,如果存在就会指向StringTable中的”def”的引用值
        String str5 = "def";
        String str6 = "abcd";//这两种不同的创建方法是有差别的,这种方式是存储到常量池并获取到其中对象的引用
        String str7 = new String("abcd");//这种方式是直接在堆内存空间创建一个新的对象,只要使用new方法,便需要创建新的对象
        System.out.println(str1 == str3);//true 
        System.out.println(str2 == str4);//false 
        System.out.println(str4 == str5);//true
        System.out.println(str6==str7);//false
    }
}

 

在将字符串常量池移动到堆中之后,对字符串的引用也发生了变化,下面说说用+连接字符串时的情况:

String str1 = "abc";
String str2 = "def";
String str3 = “abcdef”
String str4 = str1 + str2; //该语句只在堆中生成一个对象
final String str5=”ab”; 
final String str6=”cd”; 
String str7=str5+str6; 
String str8 = new String("a");
String str9 = new String("b");
String str10 = new String("c");
// 结果是true,连接生成了"abc",调用intern()后发现常量池没有"abc",那么就放入堆中的"abc"的引用,之后返回值为这个引用
//右侧直接看常量池的"abc",结果发现是堆中的"abc"的引用,直接返回这个引用,两边相同。
System.out.println((str8+str9+str10).intern() == "abc"); 
System.out.println(str3 == str4);  
System.out.println(str3 == str4.intern());  

(1)对于直接做+运算的两个字符串(字面量)常量,并不会放入字符串常量池中,而是直接把运算后的结果放入字符串常量池中

(String s = "abc"+ "def", 会直接生成“abcdef"字符串常量  而不把 "abc" "def"放进常量池) 

(2)代码中做的事情就是在字符串常量池中存储了先声明的字符串字面量常量,但是不会把使用字面量引用进行运算的结果放入字符串常量池中在堆里面存储的是"java"对象,实际上是使用StringBuilder.append来完成,会生成不同的对象。

(String s = new String("abc") + new String("def"),在构造过程中会生成“abc、def、abcdef"字符串常量,但是常量池中只有“abc”、“def”) 

(3)对于非final字段则是在运行期进行赋值处理的,而对于final字段编译期直接进行了常量替换, String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,所以常量字符串的“+”操作,编译阶段直接会合成为一个字符串。会去常量池中查找是否存在”abcd”,从而进行创建或引用。

(final String str5=”ja”; final String str6=”va”; String str7=str5+str6; 会在编译期合成一个字段,就像直接使用两个字面量)

 

用+连接字符串时,实际上是在堆内中创建对象,那么str4指向的是堆内存存储”abcdef”字符串的空间首地址,任何重新修改String都是重新分配内存空间,这就使得String对象之间互不干扰。也就是String中的内容一旦生成不可改变,直至生成新的对象

同时问题也来了,使用+连接字符串每次都生成新的对象,而且是在堆内存上进行,而相对而言堆内存速度比较慢,那么再大量连接字符串时直接+是不可取的,当然需要一种效率高的方法。Java提供的StringBuffer和StringBuilder就是解决这个问题的。区别是前者是线程安全的而后者是非线程安全的,StringBuilder在JDK1.5之后才有。不保证安全的StringBuilder有比StringBuffer更高的效率

总结一下就是JVM会对字符串常量的运算进行优化,未声明的,只放结果;已经声明的,只放声明。常量池中同时存在字符串常量和字符串引用。直接赋值和用字符串调用String构造函数都可能导致常量池中生成字符串常量;而intern()方法会尝试将堆中对象的引用放入常量池

 

四、不同情况生成字符串对象的数量问题

 

String str2 = new String("def"); 创建了几个对象? 

认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。

认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。

认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象

正确的答案:创建 1 个或者 2 个对象,这个答案的关键点在于new String 到底会不会在常量池中创建字符,通过反编译,可以使用javap -v XXX的方式查看编译的结果,其中 Constant pool 表示字符串常量池,在字符串编译期的字符串常量池中找到了 new String 定义的对象,可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。也就是说考虑类加载阶段和实际执行时会分成两个阶段创建对象:

(1)类加载对一个类只会进行一次。"def"在类加载时就已经创建并驻留了(如果该类被加载之前已经有"def"字符串被驻留过则不需要重复创建用于驻留的"def"实例)。驻留的字符串是放在全局共享的字符串常量池中的。

(2)在这段代码后续被运行的时候,"def"字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给str2持有。

String s="a"+"b"+"c"+"d"创建了几个对象(假设之前串池是空的)
StringBuilder sb = new StringBuilder();
String a = "a";
String b = "b";
String c = "c";
String d = "d";
String s = a+b+c+d;  //这句话创建了几个对象
StringBuilder sb = new StringBuilder();
sb.append("a").append("b").append("c").append("d");//这句话创建了几个对象

-------------------------------------------------------------------------------------------

答案是  7   3    0

 

第一题:“a”“b”“c”“d” “ab”“abc”“abcd”

第二题: “ab”“abc”“abcd”

第三题:因为a”“b”“c”“d”在串池中已经存在,不会创建对象,并且StringBuilder添加字符串的时候跟String

是不一样的,StringBuilder是不会创建对象的(所以说我们在增加字符串长度的时候尽量用StringBuilder,这样会少创建对象,节省资源,提高效率)

所以是0个对象

第一题中如果无(假设之前串池是空的)这句,结果则为一个对象 在JVM中有一个字符串池,它用来保存很多可以被共享的String对象,这样如果我们在使用同样字面字符串时,它就使用字符串池中同字面的字符串。常量池是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。由于常量字符串是在编译的时候就也被确定的,因"a"、"b"、"c"和"d"都是常量,因此变量s1的值在编译时就可以确定。这行代码编译后的与String s1="abcd";是一样的。

 

 

五、常量池的好处

 

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。

(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

(3)java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean。

这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 两种浮点数类型的包装类Float,Double并没有实现常量池技术。

 

猜你喜欢

转载自blog.csdn.net/ZytheMoon/article/details/105862367
今日推荐