JVM-(十)StringTable字符串常量池

本文最后更新于:June 16, 2022 pm

积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里,不积小流无以成江海。齐骥一跃,不能十步,驽马十驾,功不在舍。面对悬崖峭壁,一百年也看不出一条裂缝来,但用斧凿,能进一寸进一寸,能进一尺进一尺,不断积累,飞跃必来,突破随之。

目录

String的基本特性

  • String:字符串,使用一对双引号引起来表示。
  • String声明为final的,不可被继承。
  • String实现了Serializable接口:表示字符串是支持序列化的;实现了Comparable接口:表示String可以比较大小。
  • String在JDK8及以前内部定义了fianl char[] value用于存储字符串数据。而在JDK9时改为了byte[]数组加上一个编码标记。

String代表不可变的字符序列,简称:不可变性。

  • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
  • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  • 当调用String的replac()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

  • 字符串常量池中是不会存储相同内容的字符串。
  • String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009(JDK6,注意不同JDK版本值不一样)。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。
  • 使用 -XX:StringTableSize可以设置StringTable的长度。
  • 在JDK6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求。在JDK7中,StringTable的长度默认值是60013,从JDK8开始1009是可设置的最小值。

运行代码:(主要目的就是让程序一直处于运行状态即可)

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
37
38
39
40
41
42
43
44
package com;


import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
* @Author DragonOne
* @Date 2022/4/25 15:04
* @墨水记忆 www.tothefor.com
*/
public class AllStu {

static class ThreadTest extends Thread{

@Override
public void run() {
System.out.println("ThreadTest进入线程:"+Thread.currentThread().getName());
try {
Thread.sleep(300000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("ThreadTest线程结束:"+Thread.currentThread().getName());
}
}


public static void main(String[] args) throws Exception {

new ThreadTest().start();

}

}

class TestFinal {
public static int age = 13;
public static final int ttf = 23;
}


打开命令窗口,输入命令:

1
2
3
4
5
6
7
8
➜  ~ jps #查看进程id
2420 Jps
2117
2152 RemoteMavenServer36
2377 Launcher
2378 AllStu
➜ ~ jinfo -flag StringTableSize 2378 #替换为跑的项目的对应id
-XX:StringTableSize=60013

可以看见默认值为60013(JDK8)。

然后再通过设置虚拟机参数指定StringTable长度:-XX:StringTableSize=20。然后再跑上面两个命令,发现会报错如下:

1
2
3
StringTable size of 20 is invalid; must be between 1009 and 2305843009213693951
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

大概意思就说说:我们设置的值不对,必须是在1009到2305843009213693951之间的数。这也是上面提到过的1009是设置的最小值。

然后将20改为2000再次尝试,发现可以正常查看设置之后的值为2000。

📢注意:当前测试环境为JDK8,如果环境为JDK6,那么可以设置任何值!!!还有就是JDK7中,虽然默认值为60013,但是参数设置时依旧可以设为任意值!!

  • JDK6:默认值为1009,设置参数-XX:StringTableSize的值可以为任意值。
  • JDK7:默认值为60013,设置参数-XX:StringTableSize的值可以为任意值。
  • JDK8:默认值为60013,设置参数-XX:StringTableSize的值的最小值为1009。

String的内存分配

关于String字符串的创建和内存分配,具体也可见《JAVA知识点-深入理解String字符串的创建和比较 》

  • 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
  • 常量池就类似一个Java系统级别提供的缓存。8中基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊,主要有两种使用方式:
    • 直接使用双引号声明出来的String对象会直接存储在常量池中。如:String str = “tothefor.com”;
    • 如果不是用双引号声明的String对象,可以使用String提供的intern()方法将其放到常量池中,并返回此串的地址。如果在放入之前常量池中没有此字符串,则将此字符串放入并返回此地址;如果在放入之前常量池中就已经存在了此字符串,那么将返回已经存在的字符串的地址。

字符串常量池在JVM中的位置

  • Java 6及以前,字符串常量池存放在永久代。
  • Java 7中,字符串常量池的位置调整到了Java堆内。
    • 所有的字符串都保存在堆(heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
    • 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java7中使用String.intern()。
  • Java 8元空间,字符串常量在堆中。

intern()的使用

📢注意:在JDK6和JDK7及其之后的区别。

具体也可见《JAVA知识点-深入理解String字符串的创建和比较 》 ,这里就只是补充一点。

示例1:

1
2
3
4
5
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
// jdk6:false jdk7/8:false

这个比较简单,因为第一行的s是在堆空间中,而s2是在字符串常量池中。因为在第一行的时候,字符串常量池中就已经有了字符串”1”,所以s.intern();返回的地址就是在字符串常量池中的”1”。所以,这两个就不相等。

示例2:

1
2
3
4
5
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
//jdk6:false jdk7/8:true

JDK6:在执行String.intern()方法时,如果字符串常量池中没有当前字符串,则会创建该字符串的一个对象。

而第一行执行完后,常量池中有:”1”;堆中有:”1”、”1”、”11”。而s3就是堆中的”11”;当执行s3.intern();时,发现常量池中并没有字符串”11”,所以就会创建一个该字符串的对象。执行第三行的时候,发现常量池中已经存在了字符串”11”,所以直接返回已经存在的字符串地址。此时,s3指向的是堆中的”11”,而s4指向的是常量池中的”11”,所以两个不相等。

JDK7:在执行String.intern()方法时,如果字符串常量池中没有当前字符串,则会在常量池中创建一个指向该字符串的引用。即常量池中存储的地址就是堆中该字符串的地址,两个共用一个地址。原因:因为在JDK7及其之后,字符串常量池已经放在了堆中了,所以为了节省空间,常量池中直接引用堆(非字符串常量池)中的地址即可。

而第一行执行完后,常量池中有:”1”;堆中有:”1”、”1”、”11”。而s3就是堆中的”11”;当执行s3.intern();时,发现常量池中并没有字符串”11”,所以就会创建一个指向该字符串的引用。执行第三行的时候,发现常量池中已经存在了字符串”11”,所以直接返回已经存在的字符串地址。此时,s3指向的是堆中的”11”,而s4指向的是常量池中的”11”,但常量池中的”11”的地址其实就是堆中的”11”的地址,所以两个相等。

示例3:

1
2
3
4
5
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
//false

JDK8:而第一行执行完后,常量池中有:”1”;堆中有:”1”、”1”、”11”。而s3就是堆中的”11”;

然后执行第二行代码,因为此时在字符串常量池中还没有字符串”11”,所以此时会在字符串常量池进行创建一个字符串”11”(这里需要注意和之前的区别,这里不再是引用其他的,而是实实在在的一个对象)。

当执行s3.intern();时,发现常量池中有字符串”11”,则返回已经有的字符串的地址。这里并没有什么用。

所以,最后s3和s4是不相等的。因为s3是在堆中,而s4是在字符串常量池中。

补充:

1
2
3
4
5
String s3 = new String("1") + new String("1");
String s4 = "11";
String s5 = s3.intern();
System.out.println(s3 == s4); //false
System.out.println(s4 == s5); //true

垃圾回收

设置虚拟机参数:

1
-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails

运行代码:

1
2
3
4
5
6
public static void main(String[] args) {

for(int i=0;i<100000;++i){
String.valueOf(i).intern();
}
}

输出结果:

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
[GC (Allocation Failure) [PSYoungGen: 4096K->496K(4608K)] 4096K->584K(15872K), 0.0012761 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
PSYoungGen total 4608K, used 3946K [0x00000007bfb00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 4096K, 84% used [0x00000007bfb00000,0x00000007bfe5eb80,0x00000007bff00000)
from space 512K, 96% used [0x00000007bff00000,0x00000007bff7c010,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 11264K, used 88K [0x00000007bf000000, 0x00000007bfb00000, 0x00000007bfb00000)
object space 11264K, 0% used [0x00000007bf000000,0x00000007bf016000,0x00000007bfb00000)
Metaspace used 3259K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 12798 = 307152 bytes, avg 24.000
Number of literals : 12798 = 493288 bytes, avg 38.544
Total footprint : = 960528 bytes
Average bucket size : 0.640
Variance of bucket size : 0.641
Std. dev. of bucket size: 0.801
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 61231 = 1469544 bytes, avg 24.000
Number of literals : 61231 = 3438104 bytes, avg 56.150
Total footprint : = 5387752 bytes
Average bucket size : 1.020
Variance of bucket size : 0.797
Std. dev. of bucket size: 0.893
Maximum bucket size : 5

其中,第一行表示进行了垃圾收集,是在年轻代中进行的收集。4096K->496K表示从原来的4096K到现在的496K。

G1的String去重操作(了解)

背景:对许多Java应用(有大有小)做的测试得出的结果:

  • 堆存活数据集合里面String对象占了25%。
  • 堆存活数据集合里面重复的String对象有13.5%。
  • String对象的平均长度是45。

许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半String对象是重复的,重复的意思是说:string1.equals(string2)=true。堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。

实现

  • 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
  • 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
  • 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
  • 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
  • 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。

命令行选项

  • UseStringDeduplication(bool):开启String去重,默认是不开启的,需要手动开启。
  • PrintStringDeduplicationStatistics(bool):打印详细的去重统计信息。
  • StringDeduplicationAgeThreshold(uintx):达到这个年龄的String对象被认为是去重的候选对象。

本文作者: 墨水记忆
本文链接: https://tothefor.com/DragonOne/b0773da2.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!