☕【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是可设置的最小值;
-XX:StringTableSize=99991
分配常量池
学习好了String字符串的常量池的使用方式以后,就会理解它本身的创建以及优化机制,最后以后在遇
到此类问题就不会在出现纠结的问题。
创建方式
使用双引号方式声明对象:`String str =“String”;`,字符串(静态链接),直接存储在常量池中
使用String类提供的intern()方法,运行时常量池(动态链接)。如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中,并且返回当前常量池的地址。
/**
* 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 {@link #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. String literals are defined in section 3.10.5 of the
* The Java™ Language Specification.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
1.6版本String#intern方法中看到,这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串。如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。
1.7以后存储的是堆中对象的地址。
存放位置:
JDK6及以前,String常量池存放在永久代上;
JDK7后,字符串常量池调整存放在Java堆内;
所有的字符串都保存在堆中,和其他普通对象一样,在进行调优应用时仅需要调整堆的大小就可以了;
字符串拼接操作
public void test(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
//编译期优化
String s4 = "javaEE" + "hadoop";
System.out.println(s3 == s4);//true
//如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),
//具体的内容为拼接的结果:javaEEhadoop
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println(s6 == s7);//false
//intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址;
//如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回对象的地址。
String s8 = s6.intern();
System.out.println(s3 == s8);//true
}
5.字符串拼接的底层实现不一定使用StringBuilder,当符号拼接左右两边为字符串常量或常量引用、字面量,会在编译期优化
public void test1(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
6.StringBuilder使用优化:在基本确定字符串长度时,尽量初始化一个对应大小的初始容量值,避免扩容造成性能消耗。
7.如何保证变量s指向的是字符串常量池中的数据呢?有两种方式:
方式一: String s = "shkstart";//字面量定义的方式
方式二: 调用intern()
jdk6 和 jdk7 下 intern 的区别,上述的语句中是创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
打印结果是
jdk6 下:false false
jdk7 下:false true
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
}
打印结果为:
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 的引用地址明显不同。图中画的很清晰。

看第二段代码,从上边第二幅图中观察。
第一段代码和第二段代码的改变就是 的顺序是放在后。
这样,首先执行声明 s4 的时候常量池中是不存在对象的,执行完毕后,对象是 s4 声明产生的新对象。
然后再执行时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。
第二段代码中的 s 和 s2 代码中,s.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
小结
从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:
将String常量池从Perm区移动到了Java Heap区
方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
String s2 = "1";
System.out.println(s == s2); //jdk6:false jdk7/8:false
String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
//执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
s3.intern();//在字符串常量池中生成"11"。
// 如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。
//jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
String s4 = "11";
//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
System.out.println(s3 == s4);//jdk6:false jdk7/8:true
}
}