String 的基本操作&StringTable的相关问题
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。
package com.journaldev.test;
public class Memory {
public static void main(String[] args) { // Line 1
int i=1; // Line 2
Object obj = new Object(); // Line 3
Memory mem = new Memory(); // Line 4
mem.foo(obj); // Line 5
} // Line 9
private void foo(Object param) { // Line 6
String str = param.toString(); //// Line 7
System.out.println(str);
// 说明是在常量池中的
System.out.println(str == str.intern()); // true
} // Line 8
}
分析运行时内存(foo() 方法是实例方法,其实图中少了一个 this 局部变量)

让我们来看看程序的执行步骤。
- 一旦我们运行程序,它就会将所有运行时类加载到堆空间中。当在第 1 行找到 main() 方法时,Java 运行时创建堆栈内存以供 main() 方法线程使用。
- 我们在第 2 行创建原始局部变量,因此它被创建并存储在 main() 方法的堆栈内存中。
- 由于我们在第 3 行创建了一个 Object,它是在堆内存中创建的,堆栈内存包含它的引用。当我们在第 4 行创建 Memory 对象时,也会发生类似的过程。
- 现在,当我们在第 5 行调用 foo() 方法时,会在堆栈顶部创建一个块供 foo() 方法使用。由于 Java 是按值传递的,因此在第 6 行的 foo() 堆栈块中创建了对 Object 的新引用。
- 在第 7 行创建一个字符串,它进入堆空间的字符串池中,并在 foo() 堆栈空间中为它创建一个引用。后面那段代码说明是在常量池中
- foo() 方法在第 8 行终止,此时堆栈中为 foo() 分配的内存块变得空闲。
- 在第 9 行,main() 方法终止,为 main() 方法创建的堆栈内存被销毁。此外,程序在这一行结束,因此 Java 运行时释放所有内存并结束程序的执行。
字符串拼接操作
先说结论
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的变量
- 拼接前后,只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,根据该字符串是否在常量池中存在,分为:
- 如果存在,则返回字符串在常量池中的地址
- 如果字符串常量池中不存在该字符串,则在常量池中创建一份,并返回此对象的地址
1、常量与常量的拼接结果在常量池,原理是编译期优化
@Test
public void test1(){
String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
/*
* 最终.java编译成.class,再执行.class
* String s1 = "abc";
* String s2 = "abc"
*/
System.out.println(s1 == s2); //true
System.out.println(s1.equals(s2)); //true
}
可以看下面的字节码指令,他们都是从常量池中拿的abc

2、拼接前后,只要其中有一个是变量,结果就在堆中
调用 intern() 方法,则主动将字符串对象存入字符串常量池中,并将其地址返回(后面细说)
@Test
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";//编译期优化
//如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);//true
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
}
从字节码角度来看:拼接前后有变量,都会使用到 StringBuilder 类

字符串拼接的底层细节
举例1
@Test
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/*
如下的s1 + s2 的执行细节:(变量s是我临时定义的)
① StringBuilder s = new StringBuilder();
② s.append("a")
③ s.append("b")
④ s.toString() --> 约等于 new String("ab"),但不等价
补充:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer
*/
String s4 = s1 + s2;//
System.out.println(s3 == s4);//false
}
字节码指令
0 ldc #14 <a>
2 astore_1
3 ldc #15 <b>
5 astore_2
6 ldc #16 <ab>
8 astore_3
9 new #9 <java/lang/StringBuilder>
12 dup
13 invokespecial #10 <java/lang/StringBuilder.<init>> // 这里相当于是调用StringBuilder的构造器,new一个StringBuilder
16 aload_1 // 加载局部变量表中的a放到操作数栈中
17 invokevirtual #11 <java/lang/StringBuilder.append> // 执行StringBuilder的append()方法
20 aload_2 // 加载局部变量表中的b,放到操作数栈中
21 invokevirtual #11 <java/lang/StringBuilder.append> // 调用append()方法
24 invokevirtual #12 <java/lang/StringBuilder.toString> // 然后执行StringBuilder的toString()方法
27 astore 4 // 将s4存在局部变量表索引为4d
29 getstatic #3 <java/lang/System.out>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #4 <java/io/PrintStream.println>
46 return
举例2
/*
1. 字符串拼接操作不一定使用的是StringBuilder!
如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式。
2. 针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。
*/
@Test
public void test4(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
拼接操作与 append 操作的效率对比
/**
* 结论使用 StringBuilder 的 append() 要远高于 String 的拼接方式
* 详情:① StringBuilder 的 append() 方式:自始至终只创建过一个StringBuilder对象
* 使用String的字符串拼接方式:创建多个StringBuilder和String对象
* ② 使用String的字符串拼接方式,由于创建了较多的StringBuilder和String对象,
* 内存占用过大,如果进行GC,需要花费额外的时间
*
* StringBuilder的改进空间:在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化:
* StringBuilder s = new StringBuilder(highLevel); // new char[highLevel]
*/
@Test
public void test6(){
long start = System.currentTimeMillis();
method2(100000);// 5
// method1(100000);// 3301
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
}
/**
* 花费的时间为:3301
* @param highLevel
*/
public void method1(int highLevel){
String src = "";
for(int i = 0;i < highLevel;i++){
// 每次循环都会创建一个StringBuilder,String (toString会new String)
src = src + "a";
}
}
/**
* 花费的时间为:5
* @param highLevel
*/
public void method2(int highLevel){
// 这里只会创建一个StringBuilder
StringBuilder src = new StringBuilder(5000);
for (int i = 0; i < highLevel; i++) {
src.append("a");
}
}
结论:
-
体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式
-
原因:
- StringBuilder的append()的方式:
- 自始至终中只创建过一个StringBuilder的对象
- 使用String的字符串拼接方式:
- 创建过多个StringBuilder和String(调的toString方法)的对象,内存占用更大;
- 如果进行GC,需要花费额外的时间(在拼接的过程中产生的一些中间字符串可能永远也用不到,会产生大量垃圾字符串)。
- StringBuilder的append()的方式:
-
改进的空间:
- 在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化:
StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]- 这样可以避免频繁扩容
intern() 的使用
intern() 方法的说明
public native String intern();

-
intern是一个native方法,调用的是底层C的方法
-
字符串常量池池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被添加到池中,并返回对该字符串对象的地址。
-
如果不是用双引号声明的String对象(字面量),可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如:
String myInfo = new string("I love atguigu").intern(); -
也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true
("a"+"b"+"c").intern()=="abc" -
通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
new String() 的使用说明
new String(“ab”)会创建几个对象?
/**
* Created with IntelliJ IDEA.
* @Author: pzx
* @Date: 2022/05/28 10:45
* @Version:1.0
* 题目:
* new String("ab"); 会创建几个对象?看字节码就知道是两个了
* 一个是在堆空间中new的对象,一个是在常量池中的"ab" 字节码指令:(ldc)
*
*
*/
public class StringNewTest {
public static void main(String[] args) {
String str = new String("ab");
}
}
字节码指令
0 new #2 <java/lang/String> // 在堆空间中创建一个String对象
3 dup
4 ldc #3 <ab> // 在常量池中放入ab
6 invokespecial #4 <java/lang/String.<init>> // 在构造器中初始化
9 astore_1
10 return
0 new #2 <java/lang/String>:在堆中创建了一个 String 对象
4 ldc #3 <ab> :在字符串常量池中放入 “ab”(如果之前字符串常量池中没有 “ab” 的话)
new String(“a”) + new String(“b”) 会创建几个对象?
5个或者6个
代码
/**
* Created with IntelliJ IDEA.
* @Author: pzx
* @Date: 2022/05/28 10:45
* @Version:1.0
* 题目:
* new String("ab"); 会创建几个对象?看字节码就知道是两个了
* 一个是在堆空间中new的对象,一个是在常量池中的"ab" 字节码指令:(ldc)
*
* 思考:
* new String("a") + new String("b"); 呢?
* 对象1:new StringBuilder()
* 对象2:new String()
* 对象3:常量池中的 "a" ----> 后面调用String的构造器 String("a")
* 对象4:new String()
* 对象5:常量池中的 "b" ----> 后面调用String的构造器 String("b")
*
* 深入剖析:StringBuilder的toString():
* 对象6:new String("ab");
* 强调一下toString()的调用,在字符串常量池中,没有生成"ab"
*
*
*/
public class StringNewTest {
public static void main(String[] args) {
// String str = new String("ab");
String str = new String("a") + new String("b");
// 这下面说明了StringBuilder调用toString()没有生成"ab"
System.out.println(new StringBuilder("ab").toString() == "ab"); // false
}
}
字节码指令
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return

字节码指令分析:
0 new #2 <java/lang/StringBuilder>:拼接字符串会创建一个 StringBuilder 对象7 new #4 <java/lang/String>:创建 String 对象,对应于 new String()11 ldc #5 <a>:在字符串常量池中放入 “a”(如果之前字符串常量池中没有 “a” 的话)后面再调用String的构造器初始化 —> String(“a”)19 new #4 <java/lang/String>:创建 String 对象,对应于 new String()23 ldc #8 <b>:在字符串常量池中放入 “b”(如果之前字符串常量池中没有 “b” 的话)后面再调用String的构造器初始化 —> String(“b”)31 invokevirtual #9 <java/lang/StringBuilder.toString>:调用 StringBuilder 的 toString() 方法,会生成一个 String 对象 (强调一下toString()的调用,在字符串常量池中,没有生成"ab")
intern相关的面试题
/**
* Created with IntelliJ IDEA.
* @Author: pzx
* @Date: 2022/05/28 10:37
* @Version:1.0
*
* 如何保证s指向的是字符串常量池中的数据呢?
* 有两种方式:
* 方式一: String s = "pzx12312312"; // 字面量定义的方式
* 方式二: 调用intern()方法
* String s = new String ("abcdefg").intern();
*
*
*/
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();// 调用此方法之前,字符串常量池中已经存在1
String s2 = "1"; // 这个是在堆空间中的地址
// 但下面s还是在堆
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")的地址,这样做是为了尽量减少占用堆空间的空间
// jdk7及以后都是这样的,其实就是字符串常量池的位置发生变化,然后改进导致
String s4 = "11"; // s4记录的地址:使用的上一行代码执行时,在字符串常量池中生成的 "11" 的地址值,而这个的的地址是指向堆空间之前new String("11")的
System.out.println(s3 == s4); // jdk6: false jdk7/8: true
}
}
内存分析
JDK6 :
- new String() 即在堆中
- str.intern() 则把字符串放入常量池(jdk6是放在**方法区(永久代)**的)中,每次都是在常量池中新创建
"11"(之前没有"11"的情况下,不会指向堆空间的"11")
String s = new String("1");
s.intern();// 调用此方法之前,字符串常量池中已经存在1
String s2 = "1"; // 这个是在堆空间中的地址
System.out.println(s == s2); // false
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4); // false

JDK7及后续版本,这里是前面的"11"没有在字符串常量池中出现过,前面的s是在堆空间中的,使用intern()之后,就在字符串常量池中生成"11"的地址值,指向堆中之前创建的"11"的地址
String s = new String("1");
s.intern();// 调用此方法之前,字符串常量池中已经存在1
String s2 = "1";
System.out.println(s == s2); // false
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4); // true

面试题的拓展
/**
* StringIntern.java中练习的拓展:
*
*/
public class StringIntern1 {
public static void main(String[] args) {
//执行完下一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
String s3 = new String("1") + new String("1");//new String("11")
//在字符串常量池中生成对象"11",代码顺序换一下,实打实的在字符串常量池里有一个"11"对象
String s4 = "11";
String s5 = s3.intern();
// s3 是堆中的 "ab" ,s4 是字符串常量池中的 "ab"
System.out.println(s3 == s4);//false
// s5 是从字符串常量池中取回来的引用,当然和 s4 相等
System.out.println(s5 == s4);//true
}
}
intern()使用总结
总结String的intern()使用:
- 在jdk1.6中,将这个字符串对象尝试放入字符串常量池。
- 如果字符串常量池中已有,则不会放入。返回串池中已有的对象的地址
- 如果没有,会把此对象复制一份(相当于一个新的对象),放入串池,并且返回串池中的对象地址
- jdk1.7起,将这个字符串对象尝试放入串池。
- 如果串池中有,则不会放入。返回串池中已有的对象的地址值。
- 如果没有,则会把对象的引用地址复制一份(使用的是之前在堆空间中创建的对象的地址值),放入串池中,并返回串池中的引用地址
intern() 方法的练习(画图即可说明)
练习 1
public class StringExer1 {
public static void main(String[] args) {
String s = new String("a") + new String("b");
// 在上一行代码执行之后,字符串常量池中并没有"ab"
String s2 = s.intern(); // jdk6中:字符串常量池中创建一个字符串"ab"
/* jdk8中:串中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回 */
System.out.println(s2 == "ab"); // jdk6 true jdk7及以后 true
System.out.println(s == "ab"); // jdk6 false jdk7及以后 true
}
}
JDK6

JDK7/8

练习2
public class StringExer1 {
public static void main(String[] args) {
//加一行这个
String x = "ab";
String s = new String("a") + new String("b");//new String("ab")
String s2 = s.intern();
System.out.println(s2 == "ab");//jdk6:true jdk8:true
System.out.println(s == "ab");//jdk6:false jdk8:true
}
}

练习3
public class StringExer2 {
public static void main(String[] args) {
/* 下面主要是new("ab")和new String("a") + new String("b")的区别,
前一个会在常量池中生成"ab",后一个不会生成"ab" */
String s1=new String("ab");
// String s1 = new String("a") + new String("b");
s1.intern();
String s2 = "ab";
System.out.println(s1 == s2);
}
}
intern() 的效率测试(空间角度)
/**
* Created with IntelliJ IDEA.
* @Author: Amani
* @Date: 2022/05/28 18:57
* @Version:1.0
*
* 使用intern()测试执行效率:空间使用上
* 结论:对于程序中大量存在的字符串,尤其是存在很多重复的字符串时,使用intern()可以节省内存空间
*
*/
public class StringIntern2 {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) {
Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};
long start = System.currentTimeMillis();
for (int i = 0; i < MAX_COUNT; i++) {
// arr[i] = new String(String.valueOf(data[i % data.length]));
// 这一种没有在在对空间中创建过多的String,创建完成之后,后期会销毁
arr[i] = new String(String.valueOf(data[i % data.length])).intern();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
}
}
1、直接 new String :由于每个 String 对象都是 new 出来的,所以程序需要维护大量存放在堆空间中的 String 实例,程序内存占用也会变高
arr[i] = new String(String.valueOf(data[i % data.length]));
- 使用Java VisualVM可以看到字节数和实例数都有减少

- 使用
JProfiler进行监控
第一个
第二个
2、使用 intern() 方法:由于数组中字符串的引用都指向字符串常量池中的字符串,所以程序需要维护的 String 对象更少,内存占用也更低
//调用了intern()方法使用了字符串常量池里的字符串,那么前面堆里的字符串便会被GC掉,这也是intern省内存的关键原因
arr[i] = new String(String.valueOf(data[i % data.length])).intern();
结论:
- 对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省很大的内存空间。
- 大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用intern() 方法,就会很明显降低内存的大小。
StringTable 的垃圾回收
/**
* Created with IntelliJ IDEA.
* @Author: Amani
* @Date: 2022/05/28 19:30
* @Version:1.0
* String的垃圾回收:
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
* PrintStringTableStatistics这个参数的意思是,打印字符串常量池中的统计信息
*
*/
public class StringGCTest {
public static void main(String[] args) {
for (int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
}
}
}
输出结果:
- 在 PSYoungGen 区发生了垃圾回收
- Number of entries 和 Number of literals 明显没有 100000,说明发生了GC
- 以上两点均说明 StringTable 区发生了垃圾回收
0次到100次的时候1856-1738=118个


G1 中的 String 去重操作
Reduce the Java heap live-data set by enhancing the G1 garbage collector so that duplicate instances of String are automatically and continuously deduplicated.
其实就是就是通过增强 G1 垃圾收集器来减少 Java 堆实时数据集,以便String自动连续删除重复实例。(回收对空间中重复的String对象)
官方文档:http://openjdk.java.net/jeps/192
String去重操作的背景
注意不是字符串常量池的去重操作,字符串常量池本身就没有重复的
- 背景:对许多Java应用(有大的也有小的)做的测试得出以下结果:
- 堆存活数据集合里面String对象占了25%
- 堆存活数据集合里面重复的String对象有13.5%
- String对象的平均长度是45
- 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半String对象是重复的,重复的意思是说:
str1.equals(str2)= true。堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。
String 去重的的实现
- 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
- 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
- 使用一个HashTable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会查这个HashTable,来看堆上是否已经存在一个一模一样的char数组。
- 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
- 如果查找失败,char数组会被插入到HashTable,这样以后的时候就可以共享这个数组了。
命令行选项
UseStringDeduplication(bool):开启String去重,默认是不开启的,需要手动开启。PrintStringDeduplicationStatistics(bool):打印详细的去重统计信息StringDeduplicationAgeThreshold(uintx):达到这个年龄的String对象被认为是去重的候选对象

1038

被折叠的 条评论
为什么被折叠?



