加载中...

10-StringTable

JVM底层原理

10. StringTable

10.1. String 的基本特性

  • String:字符串,使用一对"“引起来表示
  • String 声明为 final 的,不可被继承
  • String 实现了 Serializable 接口:表示字符串是支持序列化的。
  • String 实现了 Comparable 接口:表示 string 可以比较大小
  • String 在 jdk8 及以前内部定义了 final char[] value 用于存储字符串数据。JDK9 时改为 byte[]

10.1.1. String 在 jdk9 中存储结构变更

官网地址:JEP 254: Compact Strings (java.net)

Motivation

The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.

Description

We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.

String-related classes such as AbstractStringBuilder, StringBuilder, and StringBuffer will be updated to use the same representation, as will the HotSpot VM’s intrinsic string operations.

This is purely an implementation change, with no changes to existing public interfaces. There are no plans to add any new public APIs or other interfaces.

The prototyping work done to date confirms the expected reduction in memory footprint, substantial reductions of GC activity, and minor performance regressions in some corner cases.

动机

目前 String 类的实现将字符存储在一个 char 数组中,每个字符使用两个字节(16 位)。从许多不同的应用中收集到的数据表明,字符串是堆使用的主要组成部分,此外,大多数字符串对象只包含 Latin-1 字符。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部字符数组中有一半的空间没有被使用。

说明

我们建议将 String 类的内部表示方法从 UTF-16 字符数组改为字节数组加编码标志域。新的 String 类将根据字符串的内容,以 ISO-8859-1/Latin-1(每个字符一个字节)或 UTF-16(每个字符两个字节)的方式存储字符编码。编码标志将表明使用的是哪种编码。

intern 是一个 native 方法,调用的是底层 C 的方法

1
public native String intern();
Copied!

如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法,它会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

1
String myInfo = new string("I love atguigu").intern();
Copied!

也就是说,如果在任意字符串上调用 String.intern 方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是 true

1
("a"+"b"+"c").intern() == "abc"
Copied!

通俗点讲,Interned string 就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)

image-20210511145542579

10.5.1. intern 的使用:JDK6 vs JDK7/8

 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
public static void main(String[] args) {
  /**
   * 创建了两个对象: 堆空间中一个new对象 、字符串常量池中一个字符串常量"1"(注意:此时字符串常量池中已有"1")
   * 由于字符串常量池中已存在"1"
   * s  指向的是堆空间中的对象地址
   * s2 指向的是堆空间中常量池中"1"的地址
   * 所以不相等
   */
  String s = new String("1");
  s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
  String s2 = "1";
  System.out.println(s == s2);//jdk6:false   jdk7/8:false

  /**
   * ① String s3 = new String("1") + new String("1") * 等价于new String("11"),但是,常量池中并不生成字符串"11";
   * ② s3.intern()  由于此时常量池中并无"11",所以把s3中记录的对象的地址存入常量池
   * 所以s3 和 s4 指向的都是一个地址
   */
  String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
  //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
  s3.intern();//在字符串常量池中生成"11"。如何理解:jdk6:在字符串常量池中创建了一个新的对象"11",也就有新的地址。
  //         jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
  String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
  System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
}
Copied!

image-20210511152240683

image-20200711145925091

总结 String 的 intern()的使用:

JDK1.6 中,将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址

JDK1.7 起,将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址,字符串常量池底称为hashtable结构,所以这个时候就是 {字符串变量名:堆地址}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static void main(String[] args) {
        String a = "a";//常量池中创建"a"
        String b = "b";//常量池中创建"b"

        String c = a + b;//堆中创建string对象"ab"
        String intern = c.intern();//jdk7/8:将堆的地址复制到常量池中,返回这个常量池中指向堆的地址
        System.out.println(intern == c);//true
        String d = "ab";//常量池中有"ab",并且存的是指向堆中"ab"的地址,也就是c
        System.out.println(c==d);//true
}
Copied!

练习 1

image-20200711150859709

image-20200711151326909

练习 2

image-20200711151433277

10.5.2 示例

1、

1
2
3
4
5
6
7
8
public static void main(String[] args) {
        //JDK8
        String a1 = new String("a").intern();
        String a2 = new String("a").intern();
        String a3 = "a";
        System.out.println(a1 == a2);//true
        System.out.println(a1 == a3);//true
}
Copied!

2、

1
2
3
4
5
6
7
public static void main(String[] args) {
        //JDK8
        String s1 = new String("a")+new String("b");
        s1.intern();
        String s2 = new String("a")+new String("b");
        System.out.println(s1 == s2);//false
}
Copied!

3、

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    public static void main(String[] args) {
        //JDK8
        String s1 = new String("a")+new String("b");
        String si1 = s1.intern();//s1==si1 堆中对象地址
        String s2 = new String("a")+new String("b");
        String si2 = s2.intern();//si2==s1 堆中对象地址
        System.out.println(si1 == si2);//true
        System.out.println(si2 == s1);//true
        System.out.println(s2 == si2);//false
    }
Copied!

4、

1
2
3
4
5
6
7
    public static void main(String[] args) {
        //JDK8
        String s1 = new String("ab");//会生成两个对象,一个为堆中实例,一个在字符串常量池中
        s1.intern();//直接找到字符串常量池中数据,不会在常量池中创建新的对象
        String s2 = "ab";
        System.out.println(s1 == s2);//false
    }
Copied!

10.5.3. intern 的效率测试:空间角度

我们通过测试一下,使用了 intern 和不使用的时候,其实相差还挺多的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 (Exception e) {
            e.getStackTrace();
        }
    }
}// 运行结果不使用intern7256ms使用intern1395ms
Copied!

结论:对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用 intern()方法能够节省内存空间。

大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用 intern()方法,就会很明显降低内存的大小。

10.6. StringTable 的垃圾回收

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class StringGCTest {
    /**
     * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
     */
    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            String.valueOf(i).intern();
        }
    }
}
Copied!

运行结果

image-20220424000348568

image-20220424000436391

可知现在字符串常量池中只有6万多数据,不足十万

10.7. G1 中的 String 去重操作

官网地址:JEP 192: String Deduplication in G1 (java.net)

Motivation

Many large-scale Java applications are currently bottlenecked on memory. Measurements have shown that roughly 25% of the Java heap live data set in these types of applications is consumed by String objects. Further, roughly half of those String objects are duplicates, where duplicates means string1.equals(string2) is true. Having duplicate String objects on the heap is, essentially, just a waste of memory. This project will implement automatic and continuous String deduplication in the G1 garbage collector to avoid wasting memory and reduce the memory footprint.

目前,许多大规模的 Java 应用程序在内存上遇到了瓶颈。测量表明,在这些类型的应用程序中,大约 25%的 Java 堆实时数据集被String'对象所消耗。此外,这些 "String "对象中大约有一半是重复的,其中重复意味着 "string1.equals(string2) "是真的。在堆上有重复的String’对象,从本质上讲,只是一种内存的浪费。这个项目将在 G1 垃圾收集器中实现自动和持续的`String’重复数据删除,以避免浪费内存,减少内存占用。

Licensed under CC BY-NC-SA 4.0
最后更新于 2024年5月8日 21:39
发表了90篇文章 · 总计613.28k字
本站总访问量本站访客数人次

目录