文章目录

  • 1. String的不可变性
  • 2. StringPool结构
  • 3. 字符串拼接操作
  • 4. 拼接操作与append操作效率对比
  • 5. intern详解
    • 5.1 intern基本知识
    • 5.2 new String创建了几个对象
    • 5.3 案例
    • 5.4 结论
  • 6. G1垃圾收集器下的String去重操作

1. String的不可变性

在JDK7之后,String的内容是存放在堆中的字符串常量池(StringPool,也可叫StringTable)中的,字符串常量池中相同的字符串只会创建一份,所以定义相同的字符串变量时,多个字符串变量都是指向了同一份地址中的字符串。
示例一

public class StringDemo1 {public static void main(String[] args) {String str1 = "hello world";String str2 = "hello world";if (str1 == str2){System.out.println("str1 is equal to str2");}}
}

运行,输出结果s1 is equal to s2,说明尽管str1和str2是栈帧中局部变量中两个不同的变量,但这两个变量都指向了常量池中同一份字符串,因此str1==str2。如下图所示

示例二

public class StringDemo1 {public static void main(String[] args) {String str1 = new String("hello world");String str2 = new String("hello world");if (str1 != str2){System.out.println("str1 is not equal to str2");}}
}

运行,输出结果str1 is not equal to str2,str1和str2分别指向了堆上的不同String对象,尽管两个String对象中的字符串内容都与常量池中字符串内容相同,这个后面会进一步解释,所以str1 != str2。如下图所示

2. StringPool结构

StringPool为字符串常量池,存在于堆空间中,是堆空间中专门开辟出的一块空间用于存放字符串常量池。字符串是开发中经常用到的,堆空间垃圾回收频率高,字符串放堆空间可以加快无用字符串的回收效率。在JDK7之前字符串放在方法区中(方法区在JDK7的实现叫永久代,在JDK8的实现叫元空间),方法区垃圾回收的频率较低,不利于字符串的回收。
String Pool是一个固定大小的Hashtable结构,由数组+链表形式组成,如果放进字符串常量池String Pool中的字符串很多,就会造成Hash冲突严重,从而导致链表会很长,而链表长会影响String.intern的性能大幅下降。
使用-XX:StringTableSize可以设置StringTable的长度,JDK6中String Pool的长度是固定的,只有1009长度;在JDK7中String pool的长度默认为60013,可手动设置;在JDK8中默认设置也为60013,可手动设置,但1009是可设置的最小值。

由于常量池StringPool是HashTable格式的,在常量池中同一份字符串只会保留一份,如下代码所示

public class StringDemo2 {public static void main(String[] args) {String str = "hello world";changeStr(str);System.out.println(str);    //输出:hello worldchar[] arrays = "hello world".toCharArray();changeArrays(arrays);System.out.println(arrays); //输出:yello world}public static void changeStr(String str){str = "this is changeStr";}public static void changeArrays(char[] arrays){arrays[0] = 'y';}
}

上述代码,由于常量池中字符串只保留一份,在调用changeStr时,只会创建一份新的字符串,原来的hello world并不会改变;数组分配在栈上,调用changeArrays时,传入的是数组的引用,修改了引用地址中的内容。

3. 字符串拼接操作

<1> 字符串常量和常量拼接,编译期优化,结果存储在字符串常量池中
如下案例所示,str1和str2都是常量字符串的拼接,这种常量字符串的拼接动作在编译期就会自动优化,比如str1在编译期就会被优化成String str1="abcedf",同理str2也会被优化成String str2="abcedf"。str1和str2的变量存储在栈中的局部变量表中,而str1和str2中的字符串内容存储在字符串常量池中。

public class StringDemo3 {public static void main(String[] args) {String str1 = "abc" + "def";String str2 = "ab" + "cd" + "ef";if (str1 == str2){System.out.println("str1 is equal to str2");}}
}

<2> 字符串拼接中,有一个是字符串变量,拼接后的对象存储在堆中
不同于上述示例,本例中str4由str2和str3拼接而成,而str2和str3都是变量形式,导致了虽然str1和str4的内容都是abcdef,但str1 != str4

public class StringDemo3 {public static void main(String[] args) {String str1 = "abc" + "def";String str2 = "abc";String str3 = "def";String str4 = str2 + str3;if (str1 != str4){System.out.println("str1 is not equal to str2");}}
}

为什么拼接字符串中含有变量的形式会导致字符串str1 != str4呢,java源代码编译成jvm指令如下所示,执行String str4 = str2 + str3;相当于JVM执行标记蓝色的命令。如命令所示,在做含有变量字符串拼接操作时,首先new 了一个StringBuilder对象,然后执行了StringBuilder对象的初始化方法,然后通过StringBuilder的append方法拼接字符串,拼接字符串结束后又调用了StringBuilder对象的toString方法。

StringBuilder的toString方法如下,调用该方法后,实质就是在堆中创建了一个String对象,对象中的字符串从常量池中加载,如同String的不可变性章节中的案例二,因此str1指向了常量池中字符串abcdef,而str4指向了堆中的String对象,所以两者地址不相等。

@Override
public String toString() {// Create a copy, don't share the arrayreturn new String(value, 0, count);
}

4. 拼接操作与append操作效率对比

public class StringDemo4 {public static void main(String[] args) {test1();test2();test3();}public static void test1(){long startTime = System.currentTimeMillis();String str = "";for (int i=0; i<100000; i++){str = str + "a";}long endTime = System.currentTimeMillis();System.out.println("拼接字符串花费时间:" + (endTime - startTime));   //拼接字符串花费时间:4861}public static void test2(){long startTime = System.currentTimeMillis();StringBuilder str = new StringBuilder();for (int i=0; i<100000; i++){str.append("a");}long endTime = System.currentTimeMillis();System.out.println("追加字符串花费时间:" + (endTime - startTime));   //追加字符串花费时间:9}public static void test3(){long startTime = System.currentTimeMillis();StringBuilder str = new StringBuilder(100000);for (int i=0; i<100000; i++){str.append("a");}long endTime = System.currentTimeMillis();System.out.println("指定容量追加字符串花费时间:" + (endTime - startTime));   //指定容量追加字符串花费时间:8}
}

从上述案例可以看出,拼接字符串的效率要远远低于追加字符串的效率,当然在追加字符串过程中,如果预知最终字符串长度,指定StringBuilder的容量的情况下,效率会更高。

为什么会出现追加字符串效率高于拼接字符串的效率呢?
如上述案例,对于追加字符串形式,从最开始创建了一个StringBuilder对象,然后循环100000次,调用StringBuilder对象append方法100000次。
而对于拼接字符串形式,每次拼接字符串需要创建一个StringBuilder对象和String对象,如上一章节字符串拼接操作所示,因此循环100000次就需要创建100000个StringBuilder对象和100000个String对象,因此String拼接字符串效率远远低于append字符串的效率。

5. intern详解

5.1 intern基本知识

String调用intern方法是为了确保字符串在内存中只有一份copy,这样可以节约空间,加快字符串操作任务的执行速度。当一个字符串不在常量池中,该字符串调用intern方法后,会把字符串放在常量池中一份,如果常量池中含有该字符串,就返回对该字符串的应用。如下所示:

public static void test5(){String str2 = "hello";String str1 = new String("hello").intern();System.out.println(str1 == str2); //true
}

本先str2指向字符串常量池中"hello"的地址,而str1指向堆上String地址,但是str1调用intern方法后,也指向了常量池中’hello"地址,所以str1 == str2。原理图如下:str2首先在常量池中创建了"hello"对象,str1调用了intern方法,也同时指向了常量池中"hello"的地址。

5.2 new String创建了几个对象

1. new String创建了几个字符串
String str = new String("hello world");代码为例,执行该代码后,内存中创建了几个对象呢,编译成字节码指令如下所示,首先第一行new了一个String对象,然后第3行ldc创建了一个hello world字符串的对象,并且把该字符串放到了字符串常量池中。因此String str = new String("hello world");在内存中创建了2个对象。

 0 new #4 <java/lang/String>3 dup4 ldc #12 <hello world>6 invokespecial #5 <java/lang/String.<init>>

2. new String(“hello”) + new String(“world”)创建了几个对象
以代码 String str = new String("hello") + new String("world");
为例,把该代码编译成字节码指令如下所示,首先创建了一个StringBuilder对象,然后创建了一个String对象,然后又在常量池中创建了字符串为hello的对象(ldc #3),创建的hello字符串用来初始化刚创建的String对象;随后又创建了另一个String对象,然后又在常量池中创建了字符串为world的对象(ldc #13),用来初始化另一个String对象,通过调用StringBuilder的append方法把两个字符串加起来,最后调用了StringBuilder的toString方法,本节前面例子已经说明,toString方法内部其实又创建了一个String对象,因此 new String("hello") + new String("world")总共创建了6个对象。

5.3 案例

1. 案例一

    public static void test1(){/** 前面已经介绍过,new String创建了2个对象,一个在堆中str1对象,一个在常量池中的helloworld字符串对象* 执行该执行后,在堆中创建了一个str1对象,对象中属性内容为helloworld,同时也会在常量池中创建一份helloworld字符串对象,两个对象地址不同,一个是堆上,一个是常量池中* */String str1 = new String("helloworld");/** 由于常量池中已经有helloworld对象了,执行str1.intern方法不会再向常量池中创建一份helloworld对象,该方法没有任何影响* */str1.intern();/** 由于常量池中已经有helloworld对象了,str2直接指向常量池中的helloworld对象* */String str2 = "helloworld";/** 前面已介绍,str1是堆中的内存地址,str2是常量池中内存地址,两者地址不是一个,因此不相等* */System.out.println(str1 == str2);}

str1和str2在内存中图示如下,可以看出两者地址不同。

2. 案例二

 public static void test4(){/** 前面已介绍,两个new String字符串的拼接是通过StringBuilder的append方法进行拼接的* hello和world两个字符串都已放在字符串常量池中,唯独helloworld字符串没有放在常量池* */String str1 = new String("hello") + new String("world");/** 由于常量池中还没有helloworld字符串,调用intern方法后,在常量池中创建了一份helloworld字符串对象* str1指向了堆上的String对象,堆上字符串对象存放的是常量池中helloworld对象地址* */str1.intern();/** 常量池中已经有了helloworld字符串对象,str2直接指向了常量池中helloworld字符串对象* */String str2 = "helloworld";/** str1间接指向常量池中helloworld对象,str2直接指向常量池中helloworld对象,因此str1和str2指向的地址相同* */System.out.println(str1 == str2); //true
}

str1和str2在内存中图示如下

如不特殊说明,示例均采用JDK8演示,本示例,如果改成JDK6的话,返回结果是false,因为在new String("hello") + new String("world")执行后,再执行intern方法时,并不是堆中字符串地址指向常量池中地址,而是再堆上创建了字符串后,在常量池copy同样的一份字符串,因此堆上字符串地址与常量池中字符串地址不是同一个。

String的intern方法在jdk6与jdk7/8中原理是不同的,区别如下:

JDK6中,字符串调用intern方法时

  • 如果字符串常量池中有该字符串,则不会把该字符串放入常量池;
  • 如果字符串常量池中没有,会把该字符串对象复制一份,放入常量池中,并返回常量池中字符串对象的地址。

JDK7/8中,字符串调用intern方法时

  • 如果字符串常量池中有该字符串,则不会把该字符串放入常量池;
  • 如果字符串常量池中没有,则会把该字符串对象的引用地址复制一份,放入常量池中,并返回常量池中该字符串的引用地址。

3. 案例三
案例二,调换第二条和第三条指令顺序,如下所示,结果出现翻转

    public static void test5(){String str1 = new String("hello") + new String("world");String str2 = "helloworld";/** 首先str1现在堆里面创建了“helloworld”字符串对象,并没有在字符串常量池中创建* 然后str2在字符串常量池中创建了“helloworld”字符串对象* 当str1执行intern方法时,由于常量池中已经存在了helloworld字符串对象,str1直接返回,不会把str1堆中字符串地址放到常量池中* str1和str2指向的地址不同,一个是堆中的,一个是字符串常量池中的,因此两者不相等。* */str1.intern();System.out.println(str1 == str2); //false}

str1和str2在内存中示意图如下,str1.intern()方法虽然执行了,但并没有什么影响,与不执行结果无异。

但如果改成如下形式,比较str2与str3则是相等

    String str1 = new String("hello") + new String("world");String str2 = "helloworld";String str3 = str1.intern();System.out.println(str1 == str2); //falseSystem.out.println(str2 == str3);  //true
}

4. 案例四

public static void test6(){String str1 = new String("hello") + new String("world");/** str1已经在堆上创建了helloworld对象* 调用str1.intern方法后,会把堆上helloworld字符串的引用放到字符串常量池中,* str2指向常量池中helloworld字符串对象,由于常量池中存放的是堆中helloworld字符串的引用地址,因此str2也指向了堆中helloworld对象地址* */String str2 = str1.intern();System.out.println(str1 == "helloworld");    //trueSystem.out.println(str2 == "helloworld"); //true
}

str1和str2在内存中示意图如下所示,str1与str2指向的是同一个字符串地址,因此两者相等。

但是如果对于JDK6的话,str2 == "helloworld"返回的是true,str1 == "helloworld"返回的是false。因为在执行str1.intern时,是在常量池中放的是str1中字符串对象的copy,因此str1与str2不同。

5. 案例五

public static void test7(){/** 在堆中存放了helloworld的对象,同时也会在常量池中放一份helloworld字符串对象* */String str1 = new String("helloworld");/** 由于常量池中已经有了helloworld对象,执行str1.intern方法没有任何影响* */str1.intern();/** str2指向了常量池中的helloworld字符串对象* */String str2 = "helloworld";/** str1指向的是堆中的字符串对象,str2指向的是常量池中字符串对象,因此两个地址不相容* */System.out.println(str1 == str2); //false
}

str1与str2在内存中示意图如下所示

5.4 结论

对于程序中存在大量字符串,尤其是很多重复字符串时,使用intern方法可以节省内存空间大小,例如程序中经常用到人民币、美元等字符串,如果调用intern方法,就会明显减低内存的使用大小。另外内存中对象变少,也会较少垃圾回收的次数,提供效率。

6. G1垃圾收集器下的String去重操作

G1垃圾收集器实现自动持续对重复的String对象进行去重操作,这样可以避免内存的浪费。
字符串常量池中只会存在一份字符串对象,这里说的String去重操作,指的是堆上的String对象,当重复执行new String操作时,会在堆上创建多个String对象,因此去重操作是对这一部分堆上字符串对象的去重。
步骤如下:

当垃圾收集器工作时,会访问堆上的存活对象,对每一个访问的对象都会检查是否候选的要去去重的String对象;
如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列中删除处理的对象元素,然后去尝试去重它引用的String对象;
String底层是用的一个Char型数组(JDK8),使用一个HashTable来记录所有被String对象使用不重复的Char数组。当去重的时候,会查询这个Hashtable,看判断堆上是否已经存在相同的Char数组;
如果存在,String会被调整引用Hashtable中的Char数组,释放对原来数组的引用,被释放的数组最终会被垃圾回收器进行回收;
如果不存在,Char数组就会被插入Hashtable中,方便后续共享数组。

G1下String对象去重操作归结为一句话就是:用一个Hashtable结构存储已经创建的字符串对象的Char型数组,后续创建的相同字符串的对象,在G1垃圾回收器工作时,后续创建的字符串对象都指向Hashtable中存储的Char数组,后续创建的字符串对象都会被回收,最终堆中只保留一份字符串对象。

G1垃圾回收器默认是不开启去重String对象的,如若开启,需手动设置 `-XX:+UseStringDeduplication` 。

设置-XX:+PrintStringDeduplicationStatistics可以打印字符串去重的信息。
-XX:StringDeduplicationAgeThreshold=x用来设置垃圾回收时达到指定年龄的String对象才会被去重,比如-XX:StringDeduplicationAgeThreshold=3表示3次垃圾回收之后还存在的String对象重复的才会被去重。

StringPool详解相关推荐

  1. 从命令行到IDE,版本管理工具Git详解(远程仓库创建+命令行讲解+IDEA集成使用)

    首先,Git已经并不只是GitHub,而是所有基于Git的平台,只要在你的电脑上面下载了Git,你就可以通过Git去管理"基于Git的平台"上的代码,常用的平台有GitHub.Gi ...

  2. JVM年轻代,老年代,永久代详解​​​​​​​

    秉承不重复造轮子的原则,查看印象笔记分享连接↓↓↓↓ 传送门:JVM年轻代,老年代,永久代详解 速读摘要 最近被问到了这个问题,解释的不是很清晰,有一些概念略微模糊,在此进行整理和记录,分享给大家.在 ...

  3. docker常用命令详解

    docker常用命令详解 本文只记录docker命令在大部分情境下的使用,如果想了解每一个选项的细节,请参考官方文档,这里只作为自己以后的备忘记录下来. 根据自己的理解,总的来说分为以下几种: Doc ...

  4. 通俗易懂word2vec详解词嵌入-深度学习

    https://blog.csdn.net/just_so_so_fnc/article/details/103304995 skip-gram 原理没看完 https://blog.csdn.net ...

  5. 深度学习优化函数详解(5)-- Nesterov accelerated gradient (NAG) 优化算法

    深度学习优化函数详解系列目录 深度学习优化函数详解(0)– 线性回归问题 深度学习优化函数详解(1)– Gradient Descent 梯度下降法 深度学习优化函数详解(2)– SGD 随机梯度下降 ...

  6. CUDA之nvidia-smi命令详解---gpu

    nvidia-smi是用来查看GPU使用情况的.我常用这个命令判断哪几块GPU空闲,但是最近的GPU使用状态让我很困惑,于是把nvidia-smi命令显示的GPU使用表中各个内容的具体含义解释一下. ...

  7. Bert代码详解(一)重点详细

    这是bert的pytorch版本(与tensorflow一样的,这个更简单些,这个看懂了,tf也能看懂),地址:https://github.com/huggingface/pytorch-pretr ...

  8. CRF(条件随机场)与Viterbi(维特比)算法原理详解

    摘自:https://mp.weixin.qq.com/s/GXbFxlExDtjtQe-OPwfokA https://www.cnblogs.com/zhibei/p/9391014.html C ...

  9. pytorch nn.LSTM()参数详解

    输入数据格式: input(seq_len, batch, input_size) h0(num_layers * num_directions, batch, hidden_size) c0(num ...

最新文章

  1. 使用 RPI.GPIO 模块的脉宽调制(PWM)功能
  2. C++中的基本数据类型介绍
  3. 计算机不能启动 如何排除故障,开工发现电脑无法开机 如何排查故障?
  4. MySQL主主双机负载均衡
  5. 从javascript发展说到vue
  6. Java-JUC(一):volatile引入
  7. atlas和ajaxpro以及微软企业级类库在一起得web配置文件
  8. 跳跃游戏Python解法
  9. MySql :Could not create connection to database server.
  10. php角色权限安全,php – 安全的chmod权限?
  11. 系统分析与控制_质量体系文件:测量系统分析控制程序
  12. 什么是Docker?看这一篇干货文章就够了!
  13. 一步一步SharePoint 2007之四十八:实现Excel Service(3)——调用Excel Service
  14. jvm 堆外内存_jvm┃java内存区域,跳槽大厂必会知识点
  15. 身份证号码识别(golang)
  16. asp站点服务器,ASP网站搭建 ASP服务器搭建 教程
  17. MAE 代码实战详解
  18. vue element-ui 键盘输入enter键 触发事件
  19. 基于cefsharp的浏览器应用开发(支持XP系统)
  20. Java关于身份证验证的实现

热门文章

  1. java 叠加层_java简单设置图层实现图片叠加
  2. 计算机网络教室后黑板报,教室后黑板报设计图
  3. java一个整数加100是完全平方_Java计算一个数加上100是完全平方数,加上168还是完全平方数...
  4. 20191202Spark
  5. VMware Workstation16设置共享文件夹
  6. 1068 Find More Coins (30分)
  7. 1_数据分析应掌握的Python基础
  8. Visual Paradigm 如何绘制平面图?
  9. I MM CO T-CODE
  10. android 彩信发送,在部分手机上报错,提示activityNotFoundError。