第十三章 StringTable

String 的基本特性

  • String:字符串,使用一对 "" 引起来表示
java 复制代码
// 两种定义方式
String s1 = "atguigu"; // 字面量的定义方式
String s2 = new String("hello");
  • String 声明为 final 的,不可被继承
  • String 实现了 Serializable 接口:表示字符串支持序列化的。
  • String 实现了 Comparable 接口:表示 String 可以比较大小
  • String 在 JDK 8 及以前内部定义了 final char[] value 用于存储字符串数据。JDK 9 时改为了 byte[]
  • String 代表不可变的字符序列。简称:不可变性
    • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的 value 进行复制。
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
    • 当调用 String 的 replace() 方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
  • 通过字面量的方式(区别于 new)给一个字符串复制,此时的字符串值声明在字符串常量池中。
  • 字符串常量池中是不会存储相同内容的字符串的。
    • String 的 String Pool 是一个固定大小的 Hashtable,默认值大小长度是 1009.如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降。
    • 使用 -XX:StringTableSize 可设置 StringTable 的长度
    • 在 JDK 6 中 StringTable 是固定的,就是 1009 的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize 设置没有要求。
    • 在 JDK 7 中,StringTable 的长度默认值是 60013
    • JDK 8 开始,设置 StringTable 的长度的话,1009 是可设置的最小值

[外链图片转存中...(img-9uBEhwJD-1720061415931)]

[外链图片转存中...(img-P7npCvJT-1720061415932)]
String 在 JDK 9 中存储结构的变更

官网说明:JEP 254: Compact Strings

:::warning

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.

动机

String类的当前实现将字符存储在字符数组中,每个字符使用两个字节(十六位)。从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,此外,大多数String对象仅包含Latin-1 字符。此类字符只需要一个字节的存储空间,因此此类String对象的内部字符数组中一半的空间将未使用。

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.

描述

我们建议将String类的内部表示形式从UTF-16字符数组更改为字节数组加上编码标志字段。新的字符串类将根据字符串的内容存储编码为ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的字符。编码标志将指示使用哪种编码。(即可以根据字符串的内容存储编码来使用不同的存储方式)

AbstractStringBuilder、StringBuilder和StringBuffer等字符串相关类将更新为使用相同的表示形式,HotSpot VM的内在字符串操作也是如此。

这纯粹是一种实现更改,对现有的公共接口没有更改。没有计划添加任何新的公共API或其他接口。

:::
结论:

String 再也不用 char[] 来存储了,改成了 byte[] 加上编码标记,节约了一些空间。而StringBuilder和StringBuffer等字符串相关类也更新为使用相同的表示形式。HotSpot VM的内在字符串操作也是如此。

java 复制代码
package chapter13;

import org.junit.Test;

public class StringTest1 {

    @Test
    public void test1() {
        String s1 = "abc"; // 字面量定义的方式,"abc"存储在字符串常量池中
        String s2 = "abc";

        System.out.println(s1 == s2); // true

        System.out.println(s1);
        System.out.println(s2);
    }
}

测试结果:

[外链图片转存中...(img-QpfZ90OA-1720061415932)]

java 复制代码
package chapter13;

import org.junit.Test;

public class StringTest1 {

    @Test
    public void test1() {
        String s1 = "abc"; // 字面量定义的方式,"abc"存储在字符串常量池中
        String s2 = "abc";
        s1 = "hello";

        System.out.println(s1 == s2); // 判断地址:false

        System.out.println(s1);
        System.out.println(s2);
    }

    @Test
    public void test2() {
        String s1 = "abc";
        String s2 = "abc";
        s2 += "def";
        System.out.println(s2); // abc
        System.out.println(s1); // abcdef
    }

    @Test
    public void test3() {
        String s1 = "abc";
        String s2 = s1.replace('a', 'm');
        System.out.println(s1); // abc,体现了 String 的不可变性
        System.out.println(s2); // mbc
    }
}

例题:

java 复制代码
package chapter13;

public class StringExer {
    String str = new String("good");
    char[] ch = {'t','e','s','t'};

    public void change(String str, char[] ch){
        str = "test ok";
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        StringExer ex = new StringExer();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str); // good
        System.out.println(ex.ch); // best
    }
}

结果输出:

[外链图片转存中...(img-zkC9T4MG-1720061415933)]

当主线程执行到方法 ex.chang(ex.str, ex.ch) 时,change 方法入栈,其局部变量表中包含两个变量,分别是 str 和 ch,主线程将成员变量 str 的地址值复制了一份传递给了 change 方法的局部变量 str,此时如果输出打印 str,其值将会和成员变量 str 一样,均为 good。但是 change 方法又重新将 "test ok" 赋值给了 str,由于"test ok"之前在字符串常量池中并不存在,所以虚拟机会将"test ok"添加到字符串常量池中,并将地址赋值给 str,此时 str 的值就变成了 "test ok"。随着 change 方法执行结束,虚拟机栈会将其出栈,此时的局部变量 str 也就失去了作用,所以在打印成员变量 str 的值时没有发生改变。要想改变成员变量 str 的值,则 str = "test ok"; 应该改为 this.str = "test ok";。

String 的内存分配

  • 在 Java 语言中有 8 中基本数据类型和一种比较特殊的类型 String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
  • 常量池就类似于一个 Java 系统级别提供的缓存。8 中基本数据类型的常量池都是系统协调的,String 类型的常量池比较特殊,它的主要使用方法有两种。
    • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
    • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern() 方法
    • 使用 new String("hello") 方式创建的对象,"hello"会被存放在堆中常量池之外的地方。
  • Java 6 及以前,字符串常量池存放在永久代
  • Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到 Java 堆中。
    • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时进需要调整堆大小就可以了。
    • 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由重新考虑在 Java 7 中使用 String.iintern()。
  • Java 8 元空间,字符串常量在堆。

StringTable 为什么要调整?

官网:Java SE 7 Features and Enhancements

:::warning
Area: HotSpot
Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
RFE: 6962931

:::

  • PermSize(永久代)比较小,如果大量创建字符串,永久代很容易报 OOM
  • 永久代垃圾回收频率低

String 的基本操作

案例一

java 复制代码
package chapter13;

public class StringTest4 {
    public static void main(String[] args) {
        System.out.println();
        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10"); //

        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10"); //
    }
}

[外链图片转存中...(img-r7is8Wqd-1720061415933)]

以 debug 模式启动程序:

第一个断点处:

[外链图片转存中...(img-W6SeIfEo-1720061415934)]

第二个断点处:

[外链图片转存中...(img-Iy4R4zel-1720061415934)]

继续单步执行

[外链图片转存中...(img-kxdJeTuy-1720061415934)]

直接跳到下一个断点:

[外链图片转存中...(img-Q97JAr8m-1720061415935)]

继续单步执行

[外链图片转存中...(img-jM3dWsPA-1720061415935)]

继续单步执行

[外链图片转存中...(img-3VBlVvZq-1720061415935)]

直接跳至最后一个断点

[外链图片转存中...(img-joBE9YkB-1720061415935)]

代码执行结束后,字符串个数仍为 1160,未再发生变化:

[外链图片转存中...(img-h4tofune-1720061415936)]

总结:Java 语言规范里要求完全相同的字符串字面量,应该包含同样的 Unicode 字符序列(包含同一份码点序列的常量),并且必须是指向同一个 String 类实例。

案例二

[外链图片转存中...(img-z8uLgJqC-1720061415936)]

内存分析:

[外链图片转存中...(img-hIbjFcT9-1720061415937)]

[外链图片转存中...(img-rI8hY8To-1720061415937)]

第 7 行代码创建了一个字符串。该字符串被存放在堆空间中字符串常量池中,并且在 foo() 栈空间中创建了一个指向该字符串的一个引用。

字符串拼接操作

1、常量与常量的拼接结果在常量池,原理是编译器优化

2、常量池中不会存在相同内容的常量

3、只要其中有一个是变量,结果就在堆中。变量拼接的原理是 StringBuilder

4、如果拼接的结果是调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

java 复制代码
package chapter13;

import org.junit.Test;

public class StringTest5 {

    @Test
    public void test1() {
        String s1 = "a" + "b" + "c";
        String s2 = "abc"; // "abc" 一定是放在字符串常量池中的,将此地址赋值给 s2

        /**
         * 最终 .java 编译成 .class,再执行 .class
         * 编译期就已经确定下来
         * 验证方式:可以直接在idea中查看编译后.class文件,其显示的内容如下:
         * String s1 = "abc";
         * String s2 = "abc";
         * 也可以通过 jclasslib 进行验证 
         */
        System.out.println(s1 == s2); // true
        System.out.println(s1.equals(s2)); // true
    }

    @Test
    public void test2() {
        String s1 = "javaEE";
        String s2 = "hadoop";

        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";
        // 如果拼接符号的前后出现了 new,则需要在堆空间中 new String(),具体的内容为拼接后的结果
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2;

        System.out.println(s3 == s4); // true
        System.out.println(s3 == s5); // false
        System.out.println(s3 == s6); // false
        System.out.println(s3 == s7); // false
        System.out.println(s5 == s6); // false
        System.out.println(s5 == s7); // false
        System.out.println(s6 == s7); // false

        // intern():判断字符串常量池中是否存在 javaEEhadoop 值,如果存在,则返回常量池中 javaEEhadoop 的地址值
        // 如果字符串常量池中不存在 javaEEhadoop,则向字符串常量池中添加 javaEEhadoop,并返回 javaEEhadoop 的地址值
        String s8 = s6.intern();
        System.out.println(s3 == s8); // true
    }

    @Test
    public void test3() {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4); // false
    }

     @Test
    public void test4() {
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4); // true
    }
}

test1() 方法分析:

直接在idea中查看编译后的 SpringTest5.class文件

[外链图片转存中...(img-E2vqETLQ-1720061415937)]

使用 jclsslib,可以看到两组命令完全一致。

[外链图片转存中...(img-FGk2v9nw-1720061415937)]
test3() 分析:

[外链图片转存中...(img-TWh5g7kR-1720061415938)]

s1 + s2 的执行细节如下:

:::warning

StringBuilder s = new StringBuilder(); (这里变量 s 是为了方便理解而临时定义的)

s.append("a");

s.append("b");

s.toString() --> 约等于 new String("ab");

:::

补充:在 JDK5.0 以后使用的 StringBuilder,在 JDK5.0 之前使用的 StringBuffer
test4() 分析:

字符串拼接操作不一定使用的是 StringBuilder !

如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非 StringBuilder 的方式。

在定义类、方法、基本数据类型、引用数据类型时,能使用 final 建议使用。

变量 s1、s2 被 final 修饰后则变成了常量, final 在编译的时候就会分配了,准备阶段会显示初始化。

拼接操作与 append 操作的效率对比

java 复制代码
package chapter13;

import org.junit.Test;

public class StringTest5 {
    
    @Test
    public void test6() {
        long start = System.currentTimeMillis();
//        method1(100000); //1804
        method2(100000); // 3
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:" + (end - start));
    }

    private void method2(int highLevel) {
        StringBuilder src = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            src.append("a");// 每次循环都会创建一个 StringBuilder、String
        }
    }

    private void method1(int highLevel) {
        String src = "";
        for (int i = 0; i < highLevel; i++) {
            src = src + "a";
        }
    }
}

测试 method1 :将 test6() 方法中的 method2() 注释掉

[外链图片转存中...(img-uYU75FcF-1720061415938)]

测试 method12:将 test6() 方法中的 method1() 注释掉

[外链图片转存中...(img-dhPHwtsv-1720061415938)]

总结:

通过 StringBuilder 的 append() 的方式添加字符串的效率要远高于使用 String 的字符串拼接方式。

原因:

  • StringBuilder 的 append() 的方式,自始至终只创建了一个 StringBuilder 的对象。使用 String 的字符串拼接方式,创建了多个 StringBuilder 和 String 对象
  • 使用 String 的字符串拼接方式,内存中由于创建了较多的 StringBuilder 和 String 的对象,内存占用更大;如果进行 GC,需要花费额外的时间。

使用 StringBuilder 的 append() 的方式可继续改进:

在实际开发中,如果基本确定需要添加所有的字符串长度不高于某个限定值 highLevel 的情况下,建议使用构造器 StringBuilder s = new StringBuilder(highLevel); // new char[highLevel]。

StringBuilder 底层是使用得 char 型数组存储字符串的,如果使用如果使用空参构造器,append() 方法会确认 append 进来的字符串是否超出了当前字符数组的容量,如果超出了就需要进行扩容。扩容次数过多了,效率就会下降。所以在一开始就设置好字符数组的大小,不用每次进行扩容。即使设置的容量不够,append 也会自动进行扩容的。

intern() 的使用

概述

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

  • 比如:String myInfo = new String("I love atguigu").intern();

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

:::warning

("a" + "b" + "c").intern() == "abc"

:::

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

如何保证变量 s 指向的是字符串常量池中的数据呢?

有两种方式:

  • 方式一:字面量定义的方式
    • String s = "hello";
  • 方式二:调用 intern 方法
    • String s = new String("hello").intern();
    • String s = new StringBuilder("hello").toString().intern();

JDK6 VS JDK7/8 中的intern

示例一

问题:

  • new String("ab") 会创建几个对象?
java 复制代码
package test.chapater13;

public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("ab");
    }
}

两个。一个对象是 new 关键字在堆空间中创建的。另一个对象是字符串常量池中的对象,字节码指令 ldc。 [外链图片转存中...(img-1YCwyQ4C-1720061415938)]

  • new String("a") + new String("b") 会创建几个对象?
java 复制代码
package chapter13;

public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");
    }
}

[外链图片转存中...(img-fGlwdgiK-1720061415938)]

根据字节码文件分析:

对象 1 : new StringBuilder()

对象 2 :new String("a")

对象 3 :字符串常量池中的 "a"

对象 4 :new Stirng("b")

对象 5 :字符串常量池中的 "b"

对象 6 :StringBuilder 的 toString() 方法--> new String("ab")

StringBuilder 的 toString() 方法的调用并没有在字符串常量池中生成 "ab"

[外链图片转存中...(img-G6X5i813-1720061415938)]

示例二

java 复制代码
package test.chapater13;

public class StringIntern1 {
    public static void main(String[] args) {
        String s = new String("1");
        s.intern(); // 调用此方法之前,字符串常量池中已经存在"1"
        String s2 = "1";
        System.out.println(s == s2); // jdk6: false  jdk8: false

        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 jdk8: true
    }
}

JDK6 环境下测试结果:

[外链图片转存中...(img-L6gOWZkY-1720061415939)]

变量 s 指向的是堆空间中的 String 对象,由于字符串常量池中并没有对象 "1",new 在执行时,同时向字符串常量池中添加了对象"1",变量 s2 指向的是字符串常量池中的对象"1"。所以 s == s2 的结果为 false。s.intern() 方法加不加无所谓, new String() 操作会将创建的字符串添加到字符串常量池中。

变量

s3 最终指向了堆中对象"11",根据示例一可以得知,new String("1") + new String("1"); 代码最终并没有把对象"11" 添加进字符串常量池中。s3.intern() 则会将 "11" 添加进字符串常量池。s4 指向的是字符串常量池中的对象 "11"。所以 s3 == s4 的结果为false。

[外链图片转存中...(img-RltUbUdW-1720061415939)]

JDK8 环境下测试结果:

[外链图片转存中...(img-uyLuzPrh-1720061415939)]

jdk8 中 s3.intern() 会将堆中 "11" 对象的地址拷贝一份到字符串常量池中。s4 = "11" 检测到字符串常量池中已经存在 "11",会把字符串常量池中的 "11" 的地址赋给变量 s4,即 s4 最终会指向堆中的 "11"。

[外链图片转存中...(img-kI9hv89K-1720061415939)]

示例三

java 复制代码
package chapter13;

public class StringIntern1 {
    public static void main(String[] args) {
        String s3 = new String("1") + new String("1");
        // 执行完上一行代码以后,字符串常量池中,不存在"11"
        String s4 = "11"; // 在字符串常量池中生成对象"11"
        String s5 = s3.intern();
        System.out.println(s3 == s4);
        System.out.println(s5 == s4);
    }
}

执行结果:

[外链图片转存中...(img-K9POizS2-1720061415939)]

总结 String 的 intern() 的使用

  • JDK 1.6 中,将这个字符串对象尝试放入字符串常量池。
    • 如果字符串常量池中有,则并不会放入,返回已有的字符串常量池中的对象的地址
    • 如果没有,会把此对象复制一份,放入字符串常量池,并返回字符串常量池中的对象地址。
  • JDK 1.7 起,将这个字符串对象尝试放入字符串常量池
    • 如果字符串常量池中有,则并不会放入,返回已有的字符串常量池中的对象的地址
    • 如果没有,则会把对象的引用地址复制一份,放入字符串常量池,并返回字符串常量池中的引用地址

[外链图片转存中...(img-lRIJJP59-1720061415939)]

s2 == "ab" ,最终比较的是地址值,而"ab"的地址值则是指向了字符串常量池中的"ab"。

Intern 的空间效率测试

java 复制代码
package chapter13;

public class StringIntern2 {
    static final int MAX_COUNT = 1000 * 1000;
    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 (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

未使用 intern 方法:

[外链图片转存中...(img-Lc0O47Qp-1720061415939)]

使用 intern 方法:

[外链图片转存中...(img-VhAPUHrW-1720061415940)]

可以看到在未使用 intern 方法的情况,内存的占用率要远大于使用 intern 方法的。这是因为数组 data 中的值是固定的,for 循环体是不断向字符数组中赋重复值,只不过数组索引位置不同而已。也就是说,字符串常量池中早就已经生成了"1"~"10"的对象。

java 复制代码
arr[i] = new String(String.valueOf(data[i % data.length])).intern();

这段代码的执行逻辑是这样的,首先在堆中生成 new String(String.valueOf(data[i % data.length]) 的对象,生成后判断字符串常量池中是否存在,如果存在,则将字符串常量池中的对象地址返回给 arr[i]。此时, new String(String.valueOf(data[i % data.length]) 这个对象便没有了引用,于是垃圾回收器便可以将其回收。这就是为什么使用 intern 方法后,内存空间占用较小的原因。

String str = new String("abc"):str直接用new String("abc")创建,"abc"这字符串在一出现就自动创建成对象存放到常量池中,所以常量池里面存放的是"abc"字符串的引用,并不是str创建的对象的引用。

结论:对于程序中大量存在的字符串,尤其其中存在很多复杂字符串时,使用 intern() 可以节省内存空间。

.StringTable 的垃圾回收

参数设置:

:::warning

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

:::

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 对象被认为是去重的候选对象
相关推荐
职略1 小时前
负载均衡类型和算法解析
java·运维·分布式·算法·负载均衡
A22741 小时前
LeetCode 196, 73, 105
java·算法·leetcode
容若只如初见2 小时前
项目实战--Spring Boot + Minio文件切片上传下载
java·spring boot·后端
阿里巴巴P8资深技术专家2 小时前
Java常用算法&集合扩容机制分析
java·数据结构·算法
weixin_440401692 小时前
分布式锁——基于Redis分布式锁
java·数据库·spring boot·redis·分布式
码农爱java2 小时前
Spring Boot 中的监视器是什么?有什么作用?
java·spring boot·后端·面试·monitor·监视器
zengson_g3 小时前
当需要对大量数据进行排序操作时,怎样优化内存使用和性能?
java·数据库·算法·排序算法
血战灬狂龙3 小时前
pom.xml文件加载后没有变成maven图标
xml·java·maven
无名指的等待7124 小时前
SpringBoot实现图片添加水印(完整)
java·spring boot·后端
胡尚4 小时前
Ratf协议图解、Nacos CP集群源码分析
java·spring boot