写点什么

☕【Java 技术之旅】来啊!带你认识一下 String 字符串

发布于: 2021 年 05 月 07 日
☕【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 的长度默认值是 60013StringTableSize 设置没有要求;

JDK8 中,设置 StringTable 的长度,1009 是可设置的最小值;


-XX:StringTableSize=99991
复制代码

分配常量池

学习好了 String 字符串的常量池的使用方式以后,就会理解它本身的创建以及优化机制,最后以后在遇

到此类问题就不会在出现纠结的问题。

创建方式

  • 使用双引号方式声明对象:`String str =“String”;`,字符串(静态链接),直接存储在常量池中

  • 使用 String 类提供的 intern()方法,运行时常量池(动态链接)。如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中,并且返回当前常量池的地址。

/**  * Returns a canonical representation for the string object.  * <p>  * A pool of strings, initially empty, is maintained privately by the  * class <code>String</code>.  * <p>  * When the intern method is invoked, if the pool already contains a  * string equal to this <code>String</code> object as determined by  * the {@link #equals(Object)} method, then the string from the pool is  * returned. Otherwise, this <code>String</code> object is added to the  * pool and a reference to this <code>String</code> object is returned.  * <p>  * It follows that for any two strings <code>s</code> and <code>t</code>,  * <code>s.intern() == t.intern()</code> is <code>true</code>  * if and only if <code>s.equals(t)</code> is <code>true</code>.  * <p>  * All literal strings and string-valued constant expressions are  * interned. String literals are defined in section 3.10.5 of the  * <cite>The Java™ Language Specification</cite>.  *  * @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 堆内;


所有的字符串都保存在堆中,和其他普通对象一样,在进行调优应用时仅需要调整堆的大小就可以了;

字符串拼接操作

  1. 常量与常量的拼接结果放在常量池当中, 原理是编译器优化。

  2. 常量池不会存在相同内容的常量。

  3. 只要其中有一个变量,结果就在堆中相当于 new 了一个对象

  4. 变量拼接的原理是 StringBuilder,如果循环拼接,则最好采用自己优化 StringBuilder,否则程序优化会循环创建 StringBuilder 对象。

  5. 如果拼接的结果调用 intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址

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 的引用地址明显不同。图中画的很清晰。


  • 看第二段代码,从上边第二幅图中观察。

  • 第一段代码和第二段代码的改变就是 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 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。



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 }}
复制代码


用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论 (1 条评论)

发布
用户头像
特别说明一下,new String("12")和new String("1")+new String("2"),两者的区别还有一方面在于触发在常量池中的存放的数据信息模式,此时会创建的 "12","1","2"这三个字符串常量其实不会在堆里,而是直接存放在字符串常量池中,或许如果堆中存在,会优化到堆中,之后进行更新成引用,如果:new String("a")+new String("b"),这样子的化 之后在进行实现 intern方法实现的属于运行时常量池机制,创建字符串常量池的常量对象的时候,存放的必然只是单纯的地址了,此外未来结合逃逸分析,可能压根不会存放在堆中!所以常量池中存放的(也许是常量值,也可能是堆地址值),如果我的分析有问题请大家及时反馈多多指正。
2021 年 05 月 07 日 14:06
回复
没有更多了
☕【Java技术之旅】来啊!带你认识一下String字符串