运行时常量池
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为字符串类维护了一个字符串常量池,它存在于在JVM运行时区域的方法区中,主要用来存储编译期生成的各种字面量和符号引用。
将一个.class文件进行javap反编译后,文件中有一部分是 Constant pool,其中标注了一些字面量和符号引用,但这些并不是常量池中的全部,因为在jvm运行过程中还会再加入一些
代码:
public static void main(String[] args) {
String s1 = "HardWjj";
}
进行javap反编译后
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = String #21 // HardWjj
#3 = Class #22 // com/test/Test
#4 = Class #23 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lcom/test/Test;
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 args
#15 = Utf8 [Ljava/lang/String;
#16 = Utf8 s1
#17 = Utf8 Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Test.java
#20 = NameAndType #5:#6 // "<init>":()V
#21 = Utf8 HardWjj
#22 = Utf8 com/test/Test
#23 = Utf8 java/lang/Object
其中
#16 = Utf8 s1 符号引用
#21 = Utf8 HardWjj 字面量
new String之后创建了几个对象
1、new String("字面量"):
在字符串常量池子中,相应的字面量只会存在一份,也就是说,当有新的字面量要加入常量池中时,首先会进行比较,如果池子里面已经有了,则不会再进行添加,所以在如果池子里已经有了"字面量",则只会创建一个对象在堆中,而且这个对象保存着对应常量池中相应字面量的引用。
所以:String s = new String("字面量");总共会创建两个对象,一个对象在编译器就已经确定了,在类加载的时候被创建,但是如果Constant pool已经有了,则不创建这个对象,接下来运行期会在堆中再创建一个对象,并将引用赋给栈上的变量。
2、字符串常量池的扩展:
之前有提过.class文件中Constant pool并不是常量池的全部,在运行中还会动态的往里面加一些,至于怎么加,这时就要调用String的intern方法。在String-学习笔记(1)中有提过调用intern()方法后,如果字符串常量池中已经有一个相等的字符串常量则返回其引用,如果没有则将字符串常量添加到对象池中然后再返回该字符串常量的引用。
所以intern()方法的作用是在常量该字面量的情况下,将字面量放入常量池如果有的话是返回这个常量的引用。
代码:
String s1 = "HardWjj";
String s2 = new String("HardWjj");
String s3 = new String("HardWjj").intern();
System.out.println(s1 == s2);
System.out.println(s1 == s3);
运行结果(基于java version "1.8.0_92")
false
true
在例子中,原本s1引用指向字符串常量池的一个字面量,s2指向堆中的一个对象,s3在调用intern()方法之前指向堆中的一个对象,在调用intern()方法之后,返回相应字面量在常量池中的引用,所以s1==s3,但s2始终指向堆中的一个对象,所以s1!=s2
intern的用处
如果像上面例子中的使用方式String s3 = new String("HardWjj").intern();那么就算不用item,在class文件中也会存在"HardWjj"这个字面量,所以intern()方法到底有没有用处?
public static void main(String[] args) {
String s2 = new String("HardWjj");
}
对应的.class文件
![](https://img.haomeiwen.com/i12148101/081f892289a3a986.png)
再来看下面这种情况intern()
public static void main(String[] args) {
String s1 = "Hard";
String s2 = "WJJ";
String s3 = s1 + s2;
String s4 = "Hard" + "WJJ";
}
对应的.class文件
![](https://img.haomeiwen.com/i12148101/b19c73a9789d6782.png)
通过反编译可以看出,虽然都是字符串拼接,但是s3在编译器无法确定字符串值被转成了StringBulider进行append操作,s4则直接编译为"HardWJJ"
另外,还有两个特别地方:
1、String s3 = s1 + s2;经过编译之后,常量池中存在"Hard"和"WJJ",但这两个字面量是s1和s2定义的,他们的拼接结果"HardWJJ"并不在Constant pool中
2、String s4 = "Hard" + "WJJ";经过编译后,常量池中只有"HardWJJ",并没有"Hard" ,"WJJ"这两个字符串
原因:编译后的Constant pool保存的是已经确定的字面量,如果是字面量与字面量的拼接,会在编译后将拼接结果直接放入常量池,而如果两个字符串在拼接过程中有一个不是字面量,这个时候编译器无法确定值,所以只能将值延到运行期来确定
用处:这个时候intern方法的用处就可以体现了,很多时候我们用到的字符串在编译器是无法加入常量池的,只有在运行期才能加入。如果在经常使用的字符串上使用intern定义,那么jvm在运行到这段代码的时候,就会直接去常量池中找然后进行返回,可以减少程序中new操作所耗费的时间。
所以,String s3 = new String("HardWjj").intern();的用处在于使用的intern方法,则不会在运行时再去执行复杂的new操作,而是直接返回常量池中的字面量引用。
再来看看以下的三两段代码:
代码1:
public static void main(String[] args) {
String s1 = new String("ja") + new String("va");
System.out.println(s1.intern() == s1);
}
运行结果:
false
代码2:
public static void main(String[] args) {
String s1 = new String("Hard") + new String("WJJ");
System.out.println(s1.intern() == s1);
}
运行结果:
true
上面的两段代码在编译后都会变为
String s1 = new StringBuilder().append(new String("字面量1")).append(new String("字面量2")).toString();
System.out.println(s1.intern() == s1);
首次出现原则:
在《深入理解Java虚拟机》中存在这样一句话,在jdk1.6中,intern()方法会把首次遇到的字符串实例复制到永久代,返回的也是永久代中这个字符串实例的引用......在jdk1.7后(以及部分其它虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用
所以在代码2中intern()返回的引用和由StringBulider(编译后的结果)创建的字符串实例是同一个。对于代码1比较结果为false是因为"java"(随处可见,曾经被初始化过)这个字符串在之前执行new String("ja") + new String("va");已经出现过了,不符合“首次出现原则”,所以代码1是很久之前创建过的引用与刚创建的引用进行比较肯定不相等,"HardWJJ"这个字符串是首次出现的(new StringBuilder().append(new String("Hard")).append(new String("WJJ")).toString();),所以两边引用相等,返回true
网友评论