java高级开发群青春校园开发技巧

阿里从业十年的架构师浅谈JVM系列之:String.intern

2020-07-27  本文已影响0人  Java斗帝之路

简介

StringTable是什么?它和String.intern有什么关系呢?在字符串对象的创建过程中,StringTable起到了什么作用呢?

一切的答案都在本文中,快来看看吧。

intern简介

intern是String类中的一个native方法,所以它底层是用c++来实现的。感兴趣的同学可以去查看下JVM的源码了解更多的内容。

这里我们主要谈一下intern的作用。

intern返回的是这个String所代表的对象,怎么理解呢?

String class维护了一个私有的String pool, 这个String pool也叫StringTable,中文名字叫做字符串常量池。

当我们调用intern方法的时候,如果这个StringTable中已经包含了一个相同的String对象(根据equals(Object)方法来判断两个String对象是否相等),那么将会直接返回保存在这个StringTable中的String。

如果StringTable中没有相同的对象,那么这个String对象将会被加入StringTable,并返回这个String对象的引用。

所以,当且仅当 s.equals(t) 的时候s.intern() == t.intern()。

intern和字符串字面量常量

我们知道在类文件被编译成class文件时,每个class文件都有一个常量池,常量池中存了些什么东西呢?

字符串常量,类和接口名字,字段名,和其他一些在class中引用的常量。

看一个非常简单的java类:

publicclassSimpleString{

publicString site="www.flydean.com";}

然后看一下编译出来的class文件中的Constant Pool:

Constant pool:

#1 = Methodref

#2.

#3          // java/lang/Object."":()V  #2 = Class

#4            // java/lang/Object  #3 = NameAndType

#5:

#6          // "":()V  #4 = Utf8              java/lang/Object  #5 = Utf8#6 = Utf8              ()V

#7 = String

#8            // www.flydean.com  #8 = Utf8              www.flydean.com

#9 = Fieldref

#10.

#11        // com/flydean/SimpleString.site:Ljava/lang/String;  #10 = Class

#12            // com/flydean/SimpleString  #11 = NameAndType

#13:

#14        // site:Ljava/lang/String;  #12 = Utf8              com/flydean/SimpleString  #13 = Utf8              site  #14 = Utf8              Ljava/lang/String;

#15 = Utf8              Code

#16 = Utf8              LineNumberTable

#17 = Utf8              LocalVariableTable

#18 = Utf8              this

#19 = Utf8              Lcom/flydean/SimpleString;

#20 = Utf8              SourceFile

#21 = Utf8              SimpleString.java

上面的结果,我们可以看到class常量池中的index 7存放了一个字符串,这个字符串的实际内容存放在index 8中,是一个变种的Utf8的编码。

#7=String#8// www.flydean.com 

  #8 = Utf8              www.flydean.com

好了,现在问题来了,class文件中的常量池在运行时需要转换成为JVM能够识别的运行时常量池,这个运行时的常量池和StringTable和intern有什么关系呢?

在java对象的实例化过程中,所有的字符串字面量都会在实例化的时候自动调用intern方法。

如果是第一次调用,则会创建新的String对象,存放在String Table中,并返回该String对象的引用。

分析intern返回的String对象

从上面的图中,我们也可以出来String Table中存储的是一个String对象,它和普通的String对象没有什么区别,也分为对象头,底层的byte数组引用,int hash值等。

如果你不相信,可以使用JOL来进行分析:

log.info("{}",ClassLayout.parseInstance("www.flydean.com".intern()).toPrintable());

看下输出结果:

INFO com.flydean.StringInternJOL - java.lang.String object internals:

OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)05000000(00000101000000000000000000000000)(5)

44(objectheader)00000000(00000000000000000000000000000000)(0)

84(objectheader)771a0600(01110111000110100000011000000000)(399991)

124byte[]String.value[119,119,119,46,102,108,121,100,101,97,110,46,99,111,109]

164intString.hash0

201byteString.coder0

211booleanString.hashIsZerofalse

222(lossduetothenextobjectalignment)

Instance size: 24 bytes   

Space losses:0bytes

internal+2bytesexternal=2bytestotal

分析实际的问题

有了上面的知识,让我们分析一下下面的实际问题吧:

Stringa =newString(newchar[]{'a','b','c'});

Stringb = a.intern();

System.out.println(a == b);

Stringx =newString("def");

Stringy = x.intern();

System.out.println(x == y);

两个很简单的例子,答案是什么呢?答案是true和false。

第一个例子按照上面的原理很好理解,在构建String a的时候,String table中并没有”abc“这个字符串实例。所以intern方法会将该对象添加到String table中,并返回该对象的引用。

所以a和b其实是一个对象,返回true。

那么第二个例子呢?初始化String的时候,不是也没有”def“这个字符串吗?为什么回返回false呢?

还记得我们上面一个小节分析的吗?所有的字符串字面量在初始化的时候会默认调用intern方法。

也就是说”def“在初始化的时候,已经调用了一次intern了,这个时候String table中已经有”def“这个String了。

所以x和y是两个不同的对象,返回的是false。

注意,上面的例子是在JDK7+之后运行的,如果你是在JDK6中运行,那么得到的结果都是false。

JDK6和JDK7有什么不同呢?

在JDK6中,StringTable是存放在方法区中的,而方法区是放在永久代中的。每次调用intern方法,如果String Table中不存在该String对象,则会将该String对象进行一次拷贝,并返回拷贝后String对象的引用。

因为做了一次拷贝,所以引用的不是同一个对象了。结果为false。

在JDK7之后,StringTable已经被转移到了java Heap中了,调用intern方法的时候,StringTable可以直接将该String对象加入StringTable,从而指向的是同一个对象。

G1中的去重功能

如果频繁的进行String的复制,实际上是非常消耗内存空间的。所以在G1垃圾回收器中,可以使用下面的:

-XX:+UseStringDeduplication

来开启String的去重功能。

我们还记得String对象的底层结构吧,就是一个byte[]数组,String去重的原理就是让多个字符串对象底层的byte数组指向同一个地方。从而节省内存。

我们可以通过使用:

-XX:+PrintStringTableStatistics

参数来查看StringTable的大小。并通过:

-XX:StringTableSizen=n

来指定StringTable的大小。

最后

《深入理解java虚拟机》第二版 57页

对String.intern()返回引用的测试代码如下:

结果是 :

true

false

可能很多人觉得这个结果很奇怪,在这里我们进行深入地探究。

-------------------------------转发+关注后私信“666”免费获取这本书籍的PDF版-----------------------------------

书中写道,如果JDK1.6会返回两个false,JDK1.7运行则会返回一个true一个false。

因为JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例的引用,而StringBulder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。

在JDK1.7中,intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此返回的是引用和由StringBuilder.toString()创建的那个字符串实例是同一个。

str2的比较返回false因为"java"这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串是首次出现,因此返回true。

那么就有疑问了,这个“java”字符串在哪里出现过呢?显然并不是直接出现在这个类里面。

我们分别打开String 、StringBuilder和System类的源码看看有啥发现,其中在System类里发现

根据注释可以看出来,System是由虚拟机自动调用的。

在initializeSystemClass 方法中发现调用了Version对象的init静态方法

而Version类里 laucher_name是私有静态字符串常量

因此sun.misc.Version 类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue)做默认初始化,此时被 sun.misc.Version.launcher 静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。

因此我们修改一下代码:

String str2 =newStringBuilder("Java(TM) SE ").append("Runtime Environment").toString();

System.out.println(str2.intern() == str2)

发现结果还是false

从而更加证实了我们的猜测。

再遇到类似问题的时候,希望大家可以多从源码角度去追本溯源,能够多分享出来。

转发+关注后私信“666”免费获取这本书籍的PDF版,第2版第3版打包参考;

上一篇下一篇

猜你喜欢

热点阅读