JVM内存结构笔记04-字符串常量池
文章目录
- 定义
- 字符串常量池的位置
- JDK 1.7 为什么要将字符串常量池移动到堆中?
- StringTable
- 案例1
- 案例2
- 案例3
- String.intern()
- 案例4
- 案例5
- 案例6
- 总结
- StringTable 垃圾回收案例
- 1.创建100个字符串(不会触发垃圾回收)
- 2.创建10000个字符串(触发垃圾回收)
- StringTable 性能调优
- 1.调整StringTable哈希桶个数参数:调整 -XX:StringTableSize=桶个数
- 2.考虑将字符串对象是否入池
- 常量池和运行时常量池、字符串常量池三者之间的关系
定义
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在字符串常量池中创建字符串对象 ”ab“
// 将字符串对象 ”ab“ 的引用赋值给给 aa
String aa = "ab";
// 直接返回字符串常量池中字符串对象 ”ab“,赋值给引用 bb
String bb = "ab";
System.out.println(aa==bb); // true
- HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp
- StringTable 可以简单理解为一个固定大小的HashTable ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。
- 保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
字符串常量池的位置
JDK1.7 之前,方法区的具体实现是永久代(Permanent Generation),字符串常量池存放在方法区(永久代)。
JDK 7 开始,字符串常量池和静态变量从永久代中移动到了 Java 堆中,但方法区仍然由永久代实现。这一改变主要是为了避免永久代的内存溢出问题,因为永久代的空间相对较小,且难以进行调优,而堆的管理相对更加灵活。
JDK 8 及以后的版本中,永久代被元空间(Metaspace)所取代,但字符串常量池仍然在堆中。元空间使用的是本地内存,不再受 JVM 堆内存的限制。方法区由元空间实现,它主要存储类的元数据信息,而字符串常量池独立于元空间,在堆中进行管理。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
注意:运行时常量池、方法区、字符串常量池这些都是不随虚拟机实现而改变的逻辑概念,是公共且抽象的,Metaspace、Heap 是与具体某种虚拟机实现相关的物理概念,是私有且具体的。
StringTable
String Table 是JVM内部用于管理字符串常量池的数据结构。它是实现字符串常量池的具体形式,通常实现为一个哈希表,用于存储字符串对象的引用。通过String.intern()方法可以手动将字符串添加到String Table中,从而使得这些字符串能够在整个JVM范围内共享。
案例1
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
执行javap -v Demo.class
观察
常量池中的信息,都会被加载到运行时常量池中, 这时 a、b、ab 都是常量池中的符号,还没有变为java 字符串对象。
当具体执行到指定的代码行时,才会变成java字符串对象(懒加载)。如执行:
ldc #2 会把 a 符号变为 “a” 字符串对象
ldc #3 会把 b 符号变为 “b” 字符串对象
ldc #4 会把 ab 符号变为 “ab” 字符串对象
当变为字符串对象后,会将如a为key,放入StringTable中(如果没有该key则放入)
StringTable ( “a”, “b” ,“ab” ) 是 hashTable 结构,不能扩容。
案例2
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
}
执行javap -v Demo.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder 创建StringBuilder对象
//new StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 调用无参构造方法
//new StringBuilder()
16: aload_1 //加载s1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
//new StringBuilder().append("a")
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
//new StringBuilder().append("a").append("b")
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
//new StringBuilder().append("a").append("b").toString()
27: astore 4 //s4字符串存入LocalVariableTable中的4号位置
29: return
StringBuilder中的toString方法重新创建了一个新的String字符串
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence
{
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
}
所以s3不等于s4
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()
System.out.println(s3 == s4);//false
}
案例3
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在编译期间会进行优化
//因为是两个字符串拼接而不是变量,所以结果已经在编译期确定为ab
System.out.println(s3 == s5);//true
}
执行javap -v Demo.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
String.intern()
代码示例
public static void main(String[] args) {
String s1 = "hello"; // 字面量,存储在字符串常量池
String s2 = new String("hello"); // 新对象,存储在堆中
String s3 = s2.intern(); // 将 s2 的字符串内容添加到字符串常量池
System.out.println(s1 == s2); // false,s1 在字符串常量池,s2 在堆中
System.out.println(s1 == s3); // true,s3 是字符串常量池中的引用
}
解析:
String s1 = “hello”;:
- 字符串 “hello” 存储在字符串常量池中。
- s1 直接引用字符串常量池中的对象。
String s2 = new String(“hello”);:
- 新创建的 String 对象存储在堆中。
- s2 引用堆中的对象。
String s3 = s2.intern();:
- intern() 方法将 s2 的字符串内容添加到字符串常量池(如果池中已存在,则返回池中的引用)。
- s3 引用字符串常量池中的对象。
s1 == s3 为 true:
- 因为 s1 和 s3 都引用字符串常量池中的同一个对象。
案例4
使用intern()将字符串对象尝试放入串池
public static void main(String[] args) {
String s = new String("a") + new String("b");
//注意:
// "a"和"b"是常量,所以会放在串池中
// new String("a")和new String("b")会放在堆内存中,
// s也一样,因为是两个String对象使用了StringBuilder拼接,所以生成一个新的对象new String("ab")
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
System.out.println(s == "ab");//true
System.out.println(s2 == "ab");//true
}
案例5
//先将ab放入串池
String x = "ab";
String s = new String("a") + new String("b");
String s2 = s.intern();//因为ab已经在串池中,所以没有放入,直接返回的是串池中的对象
System.out.println(s == x);//false
System.out.println(s2 == x);//true
先调用intern()
String s = new String("a") + new String("b");
String s2 = s.intern();//因为ab已经在串池中,所以没有放入,直接返回的是串池中的对象
String x = "ab";
System.out.println(s == x);//true
System.out.println(s2 == x);//true
案例6
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; //因为是字符串拼接,所以直接变为 "ab"
String s4 = s1 + s2; //因为是两个变量拼接,所以会在运行期间通过StringBuild做字符串拼接,在堆中创建新的对象
String s5 = "ab"; //因为常量池中已经有"ab",所以直接引用常量池中的对象
String s6 = s4.intern();//因为"ab"已经放入串池,所以s4没有放入串池,而是直接引用常量池中的对象
//s3在常量池中,s4在堆中
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
System.out.println("======================");
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
System.out.println(x1 == x2);//false
System.out.println("======================");
String y2 = new String("x") + new String("y");
y2.intern();
String y1 = "xy";
System.out.println(y1 == y2);//true
}
总结
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
注意:- 1.8 将这个字符串对象尝试放入字符串常量池,如果有则并不会放入,如果没有则把此对象放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入字符串常量池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回(只会将对象副本放入串池)
StringTable 垃圾回收案例
设置参数
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
打印
0
Heap
PSYoungGen total 2560K, used 1955K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 95% used [0x00000000ffd00000,0x00000000ffee8c18,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace used 3293K, capacity 4564K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13355 = 320520 bytes, avg 24.000
Number of literals : 13355 = 594648 bytes, avg 44.526
Total footprint : = 1075256 bytes
Average bucket size : 0.667
Variance of bucket size : 0.668
Std. dev. of bucket size: 0.817
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1745 = 41880 bytes, avg 24.000
Number of literals : 1745 = 177296 bytes, avg 101.602
Total footprint : = 699280 bytes
Average bucket size : 0.029
Variance of bucket size : 0.029
Std. dev. of bucket size: 0.171
Maximum bucket size : 2
重点观察其中的StringTable statistics
1.创建100个字符串(不会触发垃圾回收)
public class Demo {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100; j++) { // j=100, j=10000,100000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}`
}
}
打印
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1845 = 44280 bytes, avg 24.000
Number of literals : 1845 = 182096 bytes, avg 98.697
Total footprint : = 706480 bytes
Average bucket size : 0.031
Variance of bucket size : 0.031
Std. dev. of bucket size: 0.175
Maximum bucket size : 2
可以发现Number of entries与Number of literals新增了100个字符串对象
2.创建10000个字符串(触发垃圾回收)
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->786K(9728K), 0.0359746 secs] [Times: user=0.00 sys=0.00, real=0.04 secs]
10000
Heap
PSYoungGen total 2560K, used 851K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 17% used [0x00000000ffd00000,0x00000000ffd5ac98,0x00000000fff00000)
from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 298K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 4% used [0x00000000ff600000,0x00000000ff64a8b0,0x00000000ffd00000)
Metaspace used 3296K, capacity 4564K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13356 = 320544 bytes, avg 24.000
Number of literals : 13356 = 594664 bytes, avg 44.524
Total footprint : = 1075296 bytes
Average bucket size : 0.667
Variance of bucket size : 0.668
Std. dev. of bucket size: 0.817
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 8582 = 205968 bytes, avg 24.000
Number of literals : 8582 = 505552 bytes, avg 58.908
Total footprint : = 1191624 bytes
Average bucket size : 0.143
Variance of bucket size : 0.154
Std. dev. of bucket size: 0.393
Maximum bucket size : 3
因为创建的字符串对象没有被引用,所以无用的字符串被垃圾回收
StringTable 性能调优
原理:StringTable底层是一个hash表,哈希桶越多,元素越分散,哈希碰撞的几率变小,查找速度会变快
1.调整StringTable哈希桶个数参数:调整 -XX:StringTableSize=桶个数
注意:设置桶个数小于1009时会报错
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
StringTable size of 200 is invalid; must be between 1009 and 2305843009213693951
设置参数
-Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 演示串池大小对性能的影响
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
*/
public class Demo {
public static void main(String[] args) throws IOException {
//linux.words中大约有48万个单词
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
line.intern();
}
System.out.println("读取文件花费时间为:" + (System.nanoTime() - start) / 1000000);
}
}
}
注意:垃圾回收只有在内存紧张时才会触发
打印
读取文件花费时间为:191
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13359 = 320616 bytes, avg 24.000
Number of literals : 13359 = 594752 bytes, avg 44.521
Total footprint : = 1075456 bytes
Average bucket size : 0.668
Variance of bucket size : 0.668
Std. dev. of bucket size: 0.818
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 200000 = 1600000 bytes, avg 8.000
Number of entries : 481489 = 11555736 bytes, avg 24.000
Number of literals : 481489 = 29750392 bytes, avg 61.788
Total footprint : = 42906128 bytes
Average bucket size : 2.407
Variance of bucket size : 2.420
Std. dev. of bucket size: 1.556
Maximum bucket size : 12
设置参数
-Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
读取文件花费时间为:3685
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 15965 = 383160 bytes, avg 24.000
Number of literals : 15965 = 682432 bytes, avg 42.746
Total footprint : = 1225680 bytes
Average bucket size : 0.798
Variance of bucket size : 0.794
Std. dev. of bucket size: 0.891
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 1009 = 8072 bytes, avg 8.000
Number of entries : 482761 = 11586264 bytes, avg 24.000
Number of literals : 482761 = 29845656 bytes, avg 61.823
Total footprint : = 41439992 bytes
Average bucket size : 478.455
Variance of bucket size : 432.022
Std. dev. of bucket size: 20.785
Maximum bucket size : 547
可以看到StringTableSize变小后,向StringTable放入字符串的时间明显变长了
2.考虑将字符串对象是否入池
当有大量重复的字符串时,可以考虑使用intern()放入串池,减少字符串对象个数,节约内存
常量池和运行时常量池、字符串常量池三者之间的关系
- 常量池是.class文件的一部分(每个class文件都有自己独立的常量池),提供了类或接口在编译时所需的各种常量信息。
- 运行时常量池是JVM方法区中的一部分,当JVM加载一个类文件时,会将该类的常量池信息(常量池表)加载到方法区内的运行时常量池中。
- 字符串常量池特别针对字符串进行了优化设计,以实现字符串共享,减少内存占用。它是一个类似于哈希表(HashTable)的数据结构,在 HotSpot 虚拟机中由 StringTable 类实现。
- JDK 1.7之后运行时常量池在方法区中,字符串常量池在堆中。因此在JDK 6及之前,字符串常量池是运行时常量池的一部分,而JDK 7及之后字符串常量池被移到堆中,因此字符串常量池不再是严格意义上的运行时常量池的一部分。
- 运行时常量池中存储的是字符串常量的引用,而不是字符串对象本身。字符串对象的实际内容存储在堆中的字符串常量池。
相关文章:
JVM内存结构笔记01-运行时数据区域
JVM内存结构笔记02-堆
JVM内存结构笔记03-方法区
JVM内存结构笔记04-字符串常量池