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

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-字符串常量池


http://www.kler.cn/a/582932.html

相关文章:

  • CentOS 7系统初始化及虚拟化环境搭建手册
  • 基于django+vue的购物商城系统
  • 05.基于 TCP 的远程计算器:从协议设计到高并发实现
  • 图解AUTOSAR_CP_TcpIp
  • 1.数据清洗与预处理——Python数据挖掘(数据抽样、数据分割、异常值处理、缺失值处理)
  • 每天一道算法题【蓝桥杯】【下降路径最小和】
  • [多线程]基于阻塞队列(Blocking Queue)的生产消费者模型的实现
  • FPGA学习(三)——LED流水灯
  • 大数据实时分析:ClickHouse、Doris、TiDB 对比分析
  • 交通工具驱动电机技术解析:电瓶车、汽车、地铁与高铁的电机对比
  • 达梦数据库-学习-10-SQL 注入 HINT 规则(固定执行计划)
  • Redis Sentinel (哨兵模式)深度解析:构建高可用分布式缓存系统的核心机制
  • AI+Mermaid 制作流程图
  • 聚类中的相似矩阵和拉普拉斯矩阵
  • 计算机操作系统
  • Redis-缓存穿透击穿雪崩
  • 常见的交换机端口类型
  • k8s面经
  • 如何将错误边界与React的Suspense结合使用?
  • 随机快速排序