☕【Java 技术之旅】来啊!带你认识一下 String 字符串
技术之旅的箴言
极限就是为了超越而存在的。 —— 李浩宇.Alex
引言
Java 语言中有 8 种基本类型和一种比较特殊的类型 String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个 Java 系统级别提供的缓存。8 种基本类型的常量池都是系统协调的,String 类型的常量池比较特殊。
字符串常量池
String 的 String Pool 是一个固定大小的 Hashtable,默认值大小是长度是 1009。如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表很长的直接影响是调用 String.intern 时性能会大幅下降;
使用 -XX:StringTableSize 可以设置 StringTable 的长度
在 JDK6 中 StringTable 时固定的,就是 1009 的长度,所以常量池的字符串过多,就会导致效率下降很快,StringTableSize 设置没有要求;
在 JDK7 中,StringTable 的长度默认值是 60013,StringTableSize 设置没有要求;
在 JDK8 中,设置 StringTable 的长度,1009 是可设置的最小值;
分配常量池
学习好了 String 字符串的常量池的使用方式以后,就会理解它本身的创建以及优化机制,最后以后在遇
到此类问题就不会在出现纠结的问题。
创建方式
使用双引号方式声明对象:`String str =“String”;`,字符串(静态链接),直接存储在常量池中
使用 String 类提供的 intern()方法,运行时常量池(动态链接)。如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中,并且返回当前常量池的地址。
1.6 版本 String#intern 方法中看到,这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串。如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。
1.7 以后存储的是堆中对象的地址。
存放位置:
JDK6 及以前,String 常量池存放在永久代上;
JDK7 后,字符串常量池调整存放在 Java 堆内;
所有的字符串都保存在堆中,和其他普通对象一样,在进行调优应用时仅需要调整堆的大小就可以了;
字符串拼接操作
常量与常量的拼接结果放在常量池当中, 原理是编译器优化。
常量池不会存在相同内容的常量。
只要其中有一个变量,结果就在堆中相当于 new 了一个对象。
变量拼接的原理是 StringBuilder,如果循环拼接,则最好采用自己优化 StringBuilder,否则程序优化会循环创建 StringBuilder 对象。
如果拼接的结果调用 intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
5.字符串拼接的底层实现不一定使用 StringBuilder,当符号拼接左右两边为字符串常量或常量引用、字面量,会在编译期优化
6.StringBuilder 使用优化:在基本确定字符串长度时,尽量初始化一个对应大小的初始容量值,避免扩容造成性能消耗。
7.如何保证变量 s 指向的是字符串常量池中的数据呢?有两种方式:
方式一: String s = "shkstart";//字面量定义的方式
方式二: 调用 intern()
jdk6 和 jdk7 下 intern 的区别,上述的语句中是创建了 2 个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在 JAVA Heap 中的 String 对象。
打印结果是
jdk6 下:false false
jdk7 下:false true
打印结果为:
jdk6 下 false false
jdk7 下 false false
jdk6 中的解释
注:图中绿色线条代表 string 对象的内容指向。 黑色线条代表地址指向。
如上图所示。首先说一下 jdk6 中的情况,在 jdk6 中上述的所有打印都是 false 的,因为 jdk6 中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的。
上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用 String.intern 方法也是没有任何关系的。
jdk7 中的解释
在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有 64m,一旦常量池中大量使用 intern 是会直接产生 java.lang.OutOfMemoryError:PermGen space 错误的。
在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。正式因为字符串常量池移动到 JAVA Heap 区域后,再来解释为什么会有上述的打印结果。
在第一段代码中,先看 s3 和 s4 字符串。String s3 = new String("1") + new String("1");,这句代码中现在生成了 2 最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3 引用指向的对象。中间还有 2 个匿名的 new String("1")我们不去讨论它们。此时 s3 引用对象内容是”11″,但此时常量池中是没有 “11”对象的。
接下来 s3.intern();这一句代码,是将 s3 中的"11"字符串放入 String 常量池中,因为此时常量池中不存在"11"字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个"11"的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
最后 String s4 = "11"; 这句代码中”11″是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4 是 true。(根据堆中对象生成的)
再看 s 和 s2 对象。String s = new String("1"); 第一句代码,生成了 2 个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern();这一句是 s 对象去常量池中寻找后发现 “1”, 已经在常量池里了。(直接生成在常量池中)
接下来 String s2 = "1"; 这句代码是生成一个 s2 的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。图中画的很清晰。
看第二段代码,从上边第二幅图中观察。
第一段代码和第二段代码的改变就是
s3.intern();
的顺序是放在String s4 = "11";
后。这样,首先执行
String s4 = "11";
声明 s4 的时候常量池中是不存在“11”
对象的,执行完毕后,“11“
对象是 s4 声明产生的新对象。然后再执行
s3.intern();
时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。第二段代码中的 s 和 s2 代码中,s.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码 String s = new String("1");的时候已经生成“1”对象了。下边的 s2 声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
小结
从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括 2 点:
将 String 常量池从 Perm 区移动到了 Java Heap 区
String#intern
方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
评论 (1 条评论)