首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

浅谈String的intern

蹲厕所的熊 转载请注明原创出处,谢谢!

先来看一道题,谁能猜出它的输出结果?

答案:JDK6下输出false false,而JDK6以后输出true false。

what?为什么这种结果?我第一次看就被吓了一跳。要说原因还得从常量池的内存结构以及intern方法来说起。

我们先来说说intern方法,这个方法很有意思,我们先看看jdk对它的描述

jdk的描述很清晰,但是可能有人不知道字符串常量池是什么,我们先来看看什么是常量池。

1、什么是常量池

在Java的内存分配中,总共3种常量池:

字符串常量池

Class常量池

运行时常量池

1.1、字符串常量池

字符串常量池在Java内存区域的哪个位置?

在JDK6及之前的版本,字符串常量池是放在Perm Gen(也就是方法区)中。

在JDK7版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。

字符串常量池是什么?

在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。

在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;

在JDK7.0中,StringTable的长度可以通过参数指定:-XX:StringTableSize=66666

字符串常量池放的是什么?

在JDK6.0及之前版本中,String Pool里放的都是字符串常量;

在JDK7.0中,由于String的intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。

需要说明的是:字符串常量池中的字符串只存在一份!

上面的代码执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。

1.2、Class常量池

我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);

每个class文件都有一个class常量池。

这里有人可能要问了,什么是字面量和符号引用?

字面量包括:

文本字符串

八种基本类型的值

被声明为final的常量等

符号引用包括:

类和方法的全限定名

字段的名称和描述符

方法的名称和描述符

我们可以很容易的看到Class常量池的内容(执行 ):

下面是输出的常量池内容:

我们可以看到字符串 在#14 常量池中的定义。

在main方法的字节码指令中, 由两部分组成:

当StringTest类加载到虚拟机时,"hello world"字符串在Constant pool中使用符号引用symbol表示,当调用 指令时,如果Constant pool中索引 #2 的symbol还未解析,则调用C++底层的 方法生成char数组,并将引用保存在StringTable和常量池中,当下次调用 时,可以直接从Constant pool根据索引 #2获取 "test" 字符串的引用,避免再次到StringTable中查找。(这个在上面字符串常量池中已经说过了)

astore_1指令将"hello world"字符串的引用保存在局部变量表中。

1.3、运行时常量池

运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

2、原理分析

介绍完了常量池的概念以及不同版本的内存结构,我们再去看一开始的例子就能理解为什么JDK6下输出false false,而JDK6以后输出true false了吧。

在JDK6中,常量池在永久代分配内存,永久代和Java堆的内存是物理隔离的,执行intern方法时,如果常量池不存在该字符串,虚拟机会在常量池中复制该字符串,并返回引用,所以需要谨慎使用intern方法,避免常量池中字符串过多,导致性能变慢,甚至发生PermGen内存溢出。

在JDK7中,常量池已经在Java堆上分配内存,执行intern方法时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回,所以在JDK7中,可以重新考虑使用intern方法,减少String对象所占的内存空间。

对于变量s1,常量池中没有 "StringTest" 字符串,s1.intern() 和 s1都是指向Java对象上的String对象。

对于变量s2,常量池中一开始就已经存在 "java" 字符串,所以 s2.intern() 返回常量池中 "java" 字符串的引用。(因为像"java"这样出现率高的字符串,在虚拟机启动的时候,肯定已经使用过了)

对于这个问题的原理,早在很多年前R大就分析过了,请自行百度。

3、活学活用

偶然在知乎上发现一个问题,正好验证一下上面学的是否牢固。

在jdk7中,有以下代码:

这里的返回是两个true

这里的返回一个true一个false

注意:下面有一个重要的字节码指令ldc,ldc字节码在下面的执行语义是:到当前类的运行时常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找该index对应的项,如果该项尚未resolve则resolve之,并返回resolve后的内容。

分析过程如下。

下面是第一个方法的字节码。

【0-2】先用ldc把"a"送到栈顶,换句话说,会创建对象,并且会保存引用到字符串常量池中。

【3-22】new一个StringBuilder对象把“b”和“a”进行拼接,接着用ldc把“b”送到栈顶,创建"b"的对象,并把引用保存到字符串常量池中。接着一路append,最后调用StringBuilder对象的toString方法得到一个String对象(内容是ba,注意这个toString方法会new一个String对象),并把它赋值给param。注意,这里没有把ba的引用放入字符串常量池。

【23-40】接着调用intern去字符串常量池找有没有“ba”的引用,发现没有就会把“ba”对象的引用保存到字符串常量池,接着ldc去字符串常量池获取到刚刚保存完的地址,所以这个判断肯定是true。

【43-57】接着获取到param的引用,ldc去字符串常量池找“ba”的引用,也就是上面param指向的地址,所以这个判断肯定也是true。

我们再来看看第二个方法的字节码。

【0-2】同上。

【3-22】同上。

【23-40】这个时候和上面的不同了。它会用ldc把"ba"送到栈顶,在堆中创建一个对象,并且会保存引用到字符串常量池中。接着param的intern发现字符串常量池中已经有“ba”的引用了,就直接返回已存在的引用,但是这个引用和param指向的地址是不同的。不过这里比较的是"ba"和param.intern(),所以返回true。

【43-57】上面说了“ba”的引用和param指向的地址是不同的,所以这里返回false。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180606G0A8SA00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券