当前位置: 首页 > news >正文

JVM内存结构笔记04-字符串常量池

文章目录

  • 定义
    • 字符串常量池的位置
    • JDK 1.7 为什么要将字符串常量池移动到堆中?
  • StringTable
    • 案例1
    • 案例2
    • 案例3
    • 案例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;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=5, args_size=10: ldc           #2                  // String a2: astore_13: ldc           #3                  // String b5: astore_26: ldc           #4                  // String ab8: astore_39: new           #5                  // class java/lang/StringBuilder 创建StringBuilder对象//new StringBuilder12: dup13: 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_221: 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
{@Overridepublic String toString() {// Create a copy, don't share the arrayreturn 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 在编译期间会进行优化//因为是两个字符串拼接而不是变量,所以结果已经在编译期确定为abSystem.out.println(s3 == s5);//true}

执行javap -v Demo.class

  public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=6, args_size=10: ldc           #2                  // String a2: astore_13: ldc           #3                  // String b5: astore_26: ldc           #4                  // String ab8: astore_39: new           #5                  // class java/lang/StringBuilder12: dup13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V16: aload_117: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;20: aload_221: 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        429: ldc           #4                  // String ab31: astore        5

在这里插入图片描述

案例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");//trueSystem.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);//falseSystem.out.println(s3 == s5);//trueSystem.out.println(s3 == s6);//trueSystem.out.println("======================");String x2 = new String("c") + new String("d");String x1 = "cd";x2.intern();System.out.println(x1 == x2);//falseSystem.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
HeapPSYoungGen      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 1056768Kclass 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,100000String.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
HeapPSYoungGen      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 1056768Kclass 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()放入串池,减少字符串对象个数,节约内存


相关文章:
JVM内存结构笔记01-运行时数据区域
JVM内存结构笔记02-堆
JVM内存结构笔记03-方法区
JVM内存结构笔记04-字符串常量池


http://www.mrgr.cn/news/94178.html

相关文章:

  • 15 | 定义简洁架构 Store 层的数据类型
  • CMD批处理一些冷门命令,编写windows脚本常用?
  • 10 | 基于 Gin 实现 HTTP 服务器
  • vue 仿deepseek前端开发一个对话界面
  • 如何搭建一个适配微信小程序,h5,app的uni-app项目
  • Go Ebiten小游戏开发:俄罗斯方块
  • halcon机器人视觉(四)calibrate_hand_eye_stationary_3d_sensor
  • JAVA 基础语法备忘录 -
  • 01 | Go 项目开发极速入门课介绍
  • 如何搭建一个适配微信小程序,h5,app的工程
  • VSCode集成C语言开发环境
  • 要登录的设备ip未知时的处理方法
  • 17 | 实现简洁架构的 Biz 层
  • 【大模型】WPS 接入 DeepSeek-R1详解,打造全能AI办公助手
  • 编程助手学Python--Deepseek对OpenAI的Python库调用GPT-4模型生成对话回复理解
  • Future<V>接口 和 CompletableFuture<T>类 介绍
  • BLDC直流无刷电机转速电流双闭环调速MATLAB仿真
  • 21 | 全面测试项目功能
  • 12 | 给应用添加优雅关停功能
  • 完整项目案例:基于Django的毕业设计选题管理系统(包含源码结构、核心代码及设计文档框架)