StringTable
# String 的基本特性
String s1 = "atguigu" ; // 字面量的定义方式
String s2 = new String("hello"); // new 对象的方式
2
- String:字符串,使用一对 “” 引起来表示。
- String 类被声明为 final,不能被继承。
- String 实现了 Serializable 接口:表示字符串是支持序列化的。实现了 Comparable 接口:表示 String 可以比较大小。
- String 在 jdk8 及以前内部定义了 final char value [] 用于存储字符串数据。JDK9 时改为 byte []。
为什么 JDK9 改变了 String 的结构?
官方文档:http://openjdk.java.net/jeps/254
JDK 9 对 String 的内部实现进行了重大改变,主要是为了节约内存。在 JDK 9 之前,String 类使用 char 数组来存储字符,每个 char 占用 2 个字节。然而,大多数 String 只包含 Latin-1 字符,这些字符只需要 1 个字节就足够了。因此,即使字符串只需要 1 个字节,JVM 也会按照 2 个字节进行分配,这就浪费了一半的内存空间。
为了解决这个问题,JDK 9 对 String 的实现进行了优化。当创建一个新的字符串时,会首先检查它是否只包含 Latin-1 字符。如果是,就按照 1 字节 / 字符的规格进行内存分配。如果不是,就按照 2 字节 / 字符的规格进行分配(UTF-16 编码),从而提高了内存使用率。
这种改变带来了显著的好处:原本一个仓库装不下的零件,现在可以装下了(用更少的内存跑更大的应用);原本仓库一天往外运一次,现在可以一天半甚至两天运一次(减少 GC 次数)。
与字符串相关的类,如 AbstractStringBuilder 、 StringBuilder 和 StringBuffer 将更新为使用相同的表示法,HotSpot VM 的固有字符串操作也将更新为使用相同的表示法。
String 特性
- 不可变性:String 代表不可变的字符序列。
- 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的 value 进行赋值。
- 当对现有的字符串进行连接操作的时候,也需要重新指定内存区域赋值,不能使用原有的 value 赋值。
- 当调用 String 的 replace () 方法进行字符串修改和替换时,也需要重新指定内存区域赋值,不能使用原有 value 赋值。
/**
* 针对上面的说法做的例子
* String 的基本使用:体现 String 的不可变性
*/
public class StringTest1 {
@Test
public void test1() {
String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);//判断地址:true --> false
System.out.println(s1);//
System.out.println(s2);//abc
}
@Test
public void test2() {
String s1 = "abc";
String s2 = "abc";
s2 += "def";
System.out.println(s2);//abcdef
System.out.println(s1);//abc
}
@Test
public void test3() {
String s1 = "abc";
String s2 = s1.replace('a', 'm');
System.out.println(s1);//abc
System.out.println(s2);//mbc
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- 通过字面量的方式(区别于 new)进行赋值,此时的字符串声明在常量池中。下面一道面试题。
// 其实这个题也考了引用数据类型,和值类型的传递。
public class StringExer {
String str = new String("good");
char[] ch = {'t', 'e', 's', 't'};
public static void main(String[] args) {
StringExer ex = new StringExer();
ex.change(ex.str, ex.ch);
System.out.println(ex.str);//good
System.out.println(ex.ch);//best
}
public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'b';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
字符串常量池是不会存储相同内容的字符串的
String 的 String Table(字符串常量池)是一个固定大小的 Hashtable,默认长度为 1009,如果放入的数据特别多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后会造成调用 String.intern () 的时候性能大幅下降。
String.intern () 用于在运行期间将字符串添加到字符串常量池中。使用 - XX:StringTableSize 可设置 StringTable 的长度。
在 JDK6 中 StringTable 是固定的,就是 1009 的长度,所以如果常量池中的字符串过多就会导致效率下降很快,StringTablesize 设置没有要求。
在 JDK7 中,StringTable 的长度默认值是 60013,StringTablesize 设置没有要求。
在 JDK8 中,StringTable 的长度默认值是 60013,StringTable 可以设置的最小值为 1009。
通过下面一段代码验证一下 intern 的性能损耗。
/**
* 先执行这个代码,生成一个 word.txt
* 产生 10 万个长度不超过 10 的字符串,包含 a-z,A-Z
*/
public class GenerateString {
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("words.txt");
for (int i = 0; i < 100000; i++) {
//1 - 10
int length = (int) (Math.random() * (10 - 1 + 1) + 1);
fw.write(getString(length) + "\n");
}
fw.close();
}
public static String getString(int length) {
String str = "";
for (int i = 0; i < length; i++) {
//65 - 90, 97-122
int num = (int) (Math.random() * (90 - 65 + 1) + 65) + (int) (Math.random() * 2) * 32;
str += (char) num;
}
return str;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 性能测试
*/
public class StringTest2 {
public static void main(String[] args) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("words.txt"));
long start = System.currentTimeMillis();
String data;
while ((data = br.readLine()) != null) {
data.intern(); //如果字符串常量池中没有对应 data 的字符串的话,则在常量池中生成
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));//1009:218ms 100009:54ms
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# String 的内存分配
- 在 Java 语言中有 8 种基本数据类型和一种比较特殊的类型 String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 常量池就类似一个 Java 系统级别提供的缓存。8 种基本数据类型的常量池都是系统协调的,String 类型的常量池比较特殊。它的主要使用方法有两种。
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。比如:
String info="atguigu.com";
- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern () 方法。
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。比如:
- Java 6 及之前,字符串常量池是运行时常量池的一部分,都是放在永久代。
- Java 7 中对字符串池的逻辑做了很大变动,把字符串常量池放到了堆中,而运行时常量池还在永久代。
- 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
- 字符串常量池概念原本使用得比较多,在 Java 6 及其之前的版本中,由于字符串常量池在方法区,如果不考虑好,过度使用
String.intern()
可能会导致方法区的内存溢出,所以在这些版本中必须谨慎使用。但是这个改动使得我们有足够的理由让我们重新考虑在 Java 7 中使用 String.intern ()。
- Java 8 依旧把字符串常量池放到了堆中,而运行时常量池在元空间。
字符串常量池为什么要调整?
- 为什么要调整位置?
- 永久代的默认空间大小比较小。
- 永久代垃圾回收频率低,大量的字符串无法及时回收,容易进行 Full GC 产生 STW 或者容易产生 OOM:PermGen Space。
- 堆中空间足够大,字符串可被及时回收。
- 在 JDK 7 中,interned 字符串不再在 Java 堆的永久代中分配,而是在 Java 堆的主要部分(称为年轻代和年老代)中分配,与应用程序创建的其他对象一起分配。
- 此更改将导致驻留在主 Java 堆中的数据更多,驻留在永久生成中的数据更少,因此可能需要调整堆大小。
- 用代码演示,字符串常量池的调整。
/**
* jdk6 中:
* -XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m
*
* jdk8 中:
* -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
*/
public class StringTest3 {
public static void main(String[] args) {
//使用 Set 保持着常量池引用,避免 full gc 回收常量池行为
Set<String> set = new HashSet<String>();
//在 short 可以取值的范围内足以让 6MB 的 PermSize 或 heap 产生 OOM 了。
short i = 0;
while(true){
set.add(String.valueOf(i++).intern());
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space //异常是堆内存溢出
at java.util.HashMap.resize(HashMap.java:703)
at java.util.HashMap.putVal(HashMap.java:662)
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.add(HashSet.java:219)
at com.atguigu.java.StringTest3.main(StringTest3.java:22)
Process finished with exit code 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# String 的基本操作
检验字符串常量池的存在
public class StringTest4 {
public static void main(String[] args) {
System.out.println();//2135
System.out.println("1");//2136
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");//2146
//如下的字符串"1" 到 "10"不会再次加载
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");//2146
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在代码中打上几个断点,然后根据 DEBUG,记得要把 DEBUG 页面中的 memory 选项勾上。然后可以看到执行到第二段 123... 的时候计数已经不会增长了。说明这些数据已经在字符串常量池中。
官方的例子
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);
}//line 8
}
2
3
4
5
6
7
8
9
10
11
12
13
在 param 调用 toString 后就在字符串常量池创建了一个地址。
# 字符串拼接操作
常量与常量的拼接结果在常量池,原理是编译期优化。
点击查看
@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 }
1
2
3
4
5
6
7
8
9
10
11
12直接看字节码
0 ldc #2 <abc> 2 astore_1 3 ldc #2 <abc> 5 astore_2 // ldc 就是把字符串加载到运行时常量池,两个都是 abc,所以都是 true。
1
2
3
4
5常量池中不会存在相同内容的变量。
拼接前后,只要其中有一个是变量,结果就在堆中。变量拼接的原理是 StringBuilder。
点击查看
@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; // 在 jdk5.0 之前用的是 StringBuffer 在 5.0 之后用的 StringBuilder 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 } // 从字节码的角度来看,从 s5 开始,使用的是 StringBuilder 对象 ............... 13 new #9 <java/lang/StringBuilder> 16 dup 17 invokespecial #10 <java/lang/StringBuilder.<init> : ()V> 20 aload_1 21 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> 24 ldc #7 <hadoop> 26 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> 29 invokevirtual #12 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> 32 astore 5 ...............
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36如果左右两边都是字符串常量或者常量引用,仍然是编译期优化,而非 StringBuilder。
点击查看
public void test4() { final String s1 = "a"; final String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; System.out.println(s3 == s4);//true } //字节码文件,可以看到还是用的常量 //针对于 final 修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上 final 的时候建议使用上 0 ldc #14 <a> 2 astore_1 3 ldc #15 <b> 5 astore_2 6 ldc #16 <ab> 8 astore_3 9 ldc #16 <ab> 11 astore 4 ..............
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19使用 StringBuilder 拼接的效率要远远高于 String 拼接。
点击查看
//在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值 highLevel 的情况下, //建议使用构造器实例化:StringBuilder s = new StringBuilder(highLevel); //new char[highLevel] 这样可以避免频繁扩容。 @Test public void test6() { long start = System.currentTimeMillis(); //method1(100000);//4574ms method2(100000);//8ms long end = System.currentTimeMillis(); System.out.println("花费的时间为:" + (end - start)); } //使用 String 的方式,前面的代码字节码中也看过了使用 String 去做拼接,实际上是创建了个 StringBuilder 对象,最后会导致内存中存在过多的 String 和 StringBuilder 结果变慢,如果 GC 了还会更慢 public void method1(int highLevel) { String src = ""; for (int i = 0; i < highLevel; i++) { src = src + "a";//每次循环都会创建一个 StringBuilder、String } } //使用 StringBuilder 的 append 方法,只会创建一个 StringBuilder 对象。 public void method2(int highLevel) { //只需要创建一个 StringBuilder StringBuilder src = new StringBuilder(); for (int i = 0; i < highLevel; i++) { src.append("a"); } // System.out.println(src); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26如果拼接的结果调用 intern () 方法,根据该字符串是否在常量池中存在,分为
- 如果存在,则返回字符串在常量池中的地址。
- 如果字符串常量池中不存在该字符串,则在常量池中创建一份,并返回此对象的地址。
# intern () 的使用
intern () 说明
public native String intern();
这是一个本地方法,调用的是 C 的代码。
字符串池最初是空的,由 String 类私有维护。
当调用 intern 方法时,如果池中已包含由 equals (Object) 方法确定的等于此 String 对象的字符串,则返回池中的字符串。否则,将此 String 对象添加到池中,并返回对此 String 对象的引用。
由此可见,对于任意两个字符串 s 和 t ,当且仅当 s. equals (t) 为 true 时 s. intern () == t. intern () 为 true。点击查看
String myInfo1 = new String("I love atguigu").intern(); String myInfo2 = new String("I love atguigu"); myInfo2.intern(); System.out.println(myInfo1 == myInfo2); // 这个结果为 false,因为 myInfo2.intern();的引用被忽略了。
1
2
3
4也就是说,如果在任意字符串上调用 String.intern (),那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是 true。
("a"+"b"+"c").intern()=="abc"
1通俗点讲,Interned String 就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)。
new String () 说明
/**
* 题目:
* new String("ab")会创建几个对象?
* 1. 字符串字面量"ab",它会在字符串常量池中创建一个对象;
* 2. 通过 new 关键字,会在堆内存中另外创建一个 String 对象 s1,这个对象的值是一个对字符串常量池中"ab"的引用。
*
*/
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> // 在字符串常量池中创建了个对象
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
9 astore_1
10 return
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new String () + new String () 说明
/**
* 题目:
* new String("a") + new String("b")呢?
*/
public class StringNewTest {
public static void main(String[] args) {
String str = new String("a") + new String("b");
}
}
2
3
4
5
6
7
8
9
- 首先创建了两个对象 new String ("a") 和 new String ("b")。
- 连接符
+
会创建一个对象 StringBuilder。 - 在常量池创建两个对象 a 和 b。
- 最后 str 指向的是 StringBuilder 的 toString () 创建出的一个对象 new String ("ab")。
但是不会在字符串常量池中创建 "ab"。这是因为通过连接操作生成的字符串对象是动态创建的,而不是在编译时就确定的常量。
关于 intern 的面试题
/**
* 如何保证变量 s 指向的是字符串常量池中的数据呢?
* 有两种方式:
* 方式一: String s = "shkstart";//字面量定义的方式
* 方式二: 调用 intern()
* String s = new String("shkstart").intern();
* String s = new StringBuilder("shkstart").toString().intern();
*/
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);//jdk6:false jdk7/8:false
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//jdk6:false jdk7/8:true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
点击查看
- 首先是
String s = new String("1");
会创建两个对象,一个是 new String ("1"),另一个是在字符串常量池中的 "1"。s.intern();
调用,返回的是字符串常量池中的 "1",s2 返回的也是字符串常量池中的 "1",但是 s 指向的却是堆中 new String ("1") 的地址。所以结果为 false。 String s3 = new String("1") + new String("1");
创建了 6 个对象,但是在字符串常量池中没有创建 "11",接下来调用s3.intern();
会在字符串常量池中创建 "11" 对象,但是 jdk6 因为字符串常量池在永久代中,而 jdk7 之后把字符串常量池放到了堆中即字符串常量中的存储的是堆中 new String ("11") 的地址。这样做的目的是为了节省空间。所以 jdk6 是 false,而 jdk7 之后为 true。
关于 intern 的一些面试题拓展
public class StringExer1 {
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");//new String("ab")
//在上一行代码执行完以后,字符串常量池中并没有"ab"
String s2 = s.intern();//jdk6 中:在串池中创建一个字符串"ab"
//jdk8 中:串池中没有创建字符串"ab",而是创建一个引用,指向 new String("ab"),将此引用返回
System.out.println(s2 == "ab");//jdk6:true jdk8:true
System.out.println(s == "ab");//jdk6:false jdk8:true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
点击查看
String s = new String("a") + new String("b");
创建了 6 个对象, String s2 = s.intern();
在字符串常量池创建一个对象,然后赋值给 s2。
- jdk6:s2 和 "ab" 就都指向的是字符串常量池中的对象地址,所以 s2 和 ab 相等。而 s 指向的是堆中的 new String ("ab"),所以 s 和 "ab" 不同。
- jdk7:因为字符串常量池移到了堆中,字符串常量池中实际放的是堆的地址,所以 s 和 "ab" 的地址相同。
//这个解释就是和之前的解释一样了
public class StringExer2 {
public static void main(String[] args) {
// String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"
String s1 = new String("a") + new String("b");////执行完以后,不会在字符串常量池中会生成"ab"
s1.intern();
String s2 = "ab";
System.out.println(s1 == s2);
}
}
2
3
4
5
6
7
8
9
10
intern () 的效率测试(空间角度)
/**
* 使用 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]));
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();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
点击查看
通过查看 JProfiler 或者 JVisualvm 可以看到,如果在使用大量字符串的前提下,使用 intern 方法可以节省大量的内存空间。主要是因为一旦用了 intern 那么就是在字符串常量池中创建,这样就不需要维护堆内存中的实例对象。
总结 intern ()
- jdk6 中将这个字符串对象尝试放入串池。
- 如果池中有,则不放入,返回已有对象的串池地址。
- 如果对象中没有,会把此对象复制一份,放入串池,并返回串池中的对象地址。
- jdk7 起将这个字符串对象尝试放入串池。
- 如果池中有,则不放入,返回已有对象的串池地址。
- 如果没有,则会把对象的引用地址放入串池,并返回对象的引用地址。
# StringTable 的垃圾回收
/**
* 通过修改循环的次数来看垃圾回收。
* String 的垃圾回收:
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
*/
public class StringGCTest {
public static void main(String[] args) {
for (int j = 0; j < 100000/**10/1000**/; j++) {
String.valueOf(j).intern();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
在循环为 10 的时候,没有发生 GC,并且在 StringTable statistics
中也可以看到,字符串常量池中的数据量、大小。
在将循环改成 10 万次后,在年轻代发生了 GC。至于说这个显示的 60013 不足 10 万条数据,并非是将那 4 万条数据给清空了,而是通过哈希到了同一个桶,形成了链表。
# G1 中的 String 去重操作
官方描述:JEP 192: String Deduplication in G1 (openjdk.org) (opens new window)
注意不是字符串常量池的去重操作,字符串常量池本身就没有重复的。
背景
- 对许多 Java 应用(有大的也有小的)做的测试得出以下结果:
- 堆存活数据集合里面 String 对象占了 25%。
- 堆存活数据集合里面重复的 String 对象有 13.5%。
- String 对象的平均长度是 45。
- 目前,许多大型 Java 应用程序都存在内存瓶颈。测量结果表明,在这类应用程序中,Java 堆实时数据集约有 25% 被
String
对象占用。此外,在这些String
对象中,大约有一半是重复的,重复意味着string1.equals(string2)
属实。堆上有重复的String
对象,从本质上讲就是浪费内存。本项目将在 G1 垃圾收集器中实现自动和持续的String
重复数据删除,以避免浪费内存并减少内存占用。
实现
- 标记阶段:在 G1 的并发标记阶段,会扫描整个堆,找出所有的字符串对象。
- 去重阶段:在标记阶段结束后,会启动一个名为 StringDeduplicationTable 的哈希表,用于存储所有唯一的字符串。然后,G1 会启动一个后台线程(StringDedupThread),这个线程会遍历所有的字符串对象,对每个字符串对象,都会检查 StringDeduplicationTable 中是否已经存在相同的字符串。如果存在,就将当前字符串对象的引用指向 StringDeduplicationTable 中的字符串;如果不存在,就将当前字符串添加到 StringDeduplicationTable 中。
- 清理阶段:在下一次 G1 的并发标记阶段,会清理掉所有不再使用的字符串。
命令
- -XX:+UseG1GC:使用 G1 垃圾回收器。
- -XX:+UseStringDeduplication:开启 String 去重,默认是不开启的,需要手动开启。
- -XX:+PrintStringDeduplicationStatistics:打印详细的去重统计信息。
- -XX:StringDeduplicationAgeThreshold=2:一个对象只需要经历 2 次垃圾收集,就有可能被考虑进行去重操作。