笔记 笔记
首页
  • 开发工具
  • Java Web
  • Java 进阶
  • 容器化技术
  • Java 专栏

    • Java 核心技术面试精讲
    • Java 业务开发常见错误 100 例
  • 数据库专栏

    • MySQL 实战 45 讲
    • Redis 核心技术与实战
  • 安全专栏

    • OAuth 2.0 实战课
  • 计算机系统
  • 程序设计语言
  • 数据结构
  • 知识产权
  • 数据库
  • 面向对象
  • UML
  • 设计模式
  • 操作系统
  • 结构化开发
  • 软件工程
  • 计算机网络
  • 上午题错题
在线工具 (opens new window)

EasT-Duan

Java 开发
首页
  • 开发工具
  • Java Web
  • Java 进阶
  • 容器化技术
  • Java 专栏

    • Java 核心技术面试精讲
    • Java 业务开发常见错误 100 例
  • 数据库专栏

    • MySQL 实战 45 讲
    • Redis 核心技术与实战
  • 安全专栏

    • OAuth 2.0 实战课
  • 计算机系统
  • 程序设计语言
  • 数据结构
  • 知识产权
  • 数据库
  • 面向对象
  • UML
  • 设计模式
  • 操作系统
  • 结构化开发
  • 软件工程
  • 计算机网络
  • 上午题错题
在线工具 (opens new window)

购买兑换码请添加

添加时候请写好备注,否则无法通过。

  • 设计模式

  • JVM 详解

    • JVM 与 Java 体系结构
    • 类加载子系统
    • 运行时数据区
    • 程序计数器
    • 虚拟机栈
    • 本地方法接口
    • 本地方法栈
    • 堆
    • 方法区
    • 对象的实例化内存布局与访问定位
    • 直接内存
    • 执行引擎
    • StringTable
      • String 的基本特性
      • String 的内存分配
      • String 的基本操作
      • 字符串拼接操作
      • intern() 的使用
      • StringTable 的垃圾回收
      • G1 中的 String 去重操作
    • 垃圾回收概述
    • 垃圾回收算法
    • 垃圾回收概念
    • 垃圾回收器
  • Linux

  • Redis

  • 分布式锁

  • Shiro

  • Gradle

  • Java 进阶
  • JVM 详解
East 东
2024-04-11
目录

StringTable

欢迎来到我的 ChatGPT 中转站,极具性价比,为付费不方便的朋友提供便利,有需求的可以添加左侧 QQ 二维码,另外,邀请新用户能获取余额哦!最后说一句,那啥:请自觉遵守《生成式人工智能服务管理暂行办法》。

# String 的基本特性

String s1 = "atguigu" ;   			// 字面量的定义方式
String s2 =  new String("hello");     // new 对象的方式
1
2
  • String:字符串,使用一对 “” 引起来表示。
  • String 类被声明为 final,不能被继承。
  • String 实现了 Serializable 接口:表示字符串是支持序列化的。实现了 Comparable 接口:表示 String 可以比较大小。
  • String 在 jdk8 及以前内部定义了 final char value [] 用于存储字符串数据。JDK9 时改为 byte []。

为什么 JDK9 改变了 String 的结构?

官方文档:http://openjdk.java.net/jeps/254

JDK 9 对 String 的内部实现进行了重大改变,主要是为了节约内存。在 JDK 9 之前,String 类使用 char 数组来存储字符,每个 char 占用 2 个字节。然而,大多数 String 只包含 Latin-1 字符,这些字符只需要 1 个字节就足够了。因此,即使字符串只需要 1 个字节,JVM 也会按照 2 个字节进行分配,这就浪费了一半的内存空间。

为了解决这个问题,JDK 9 对 String 的实现进行了优化。当创建一个新的字符串时,会首先检查它是否只包含 Latin-1 字符。如果是,就按照 1 字节 / 字符的规格进行内存分配。如果不是,就按照 2 字节 / 字符的规格进行分配(UTF-16 编码),从而提高了内存使用率。

这种改变带来了显著的好处:原本一个仓库装不下的零件,现在可以装下了(用更少的内存跑更大的应用);原本仓库一天往外运一次,现在可以一天半甚至两天运一次(减少 GC 次数)。

与字符串相关的类,如 AbstractStringBuilder 、 StringBuilder 和 StringBuffer 将更新为使用相同的表示法,HotSpot VM 的固有字符串操作也将更新为使用相同的表示法。

String 特性

  • 不可变性:String 代表不可变的字符序列。
    • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的 value 进行赋值。
    • 当对现有的字符串进行连接操作的时候,也需要重新指定内存区域赋值,不能使用原有的 value 赋值。
    • 当调用 String 的 replace () 方法进行字符串修改和替换时,也需要重新指定内存区域赋值,不能使用原有 value 赋值。
/**
 * 针对上面的说法做的例子
 * String 的基本使用:体现 String 的不可变性
 */
public class StringTest1 {
    @Test
    public void test1() {
        String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中
        String s2 = "abc";
        s1 = "hello";

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

        System.out.println(s1);//
        System.out.println(s2);//abc

    }

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

    @Test
    public void test3() {
        String s1 = "abc";
        String s2 = s1.replace('a', 'm');
        System.out.println(s1);//abc
        System.out.println(s2);//mbc
    }
}
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
26
27
28
29
30
31
32
33
34
35
  • 通过字面量的方式(区别于 new)进行赋值,此时的字符串声明在常量池中。下面一道面试题。
// 其实这个题也考了引用数据类型,和值类型的传递。
public class StringExer {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    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
    }

    public void change(String str, char ch[]) {
        str = "test ok";
        ch[0] = 'b';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

字符串常量池是不会存储相同内容的字符串的

  • String 的 String Table(字符串常量池)是一个固定大小的 Hashtable,默认长度为 1009,如果放入的数据特别多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后会造成调用 String.intern () 的时候性能大幅下降。

    String.intern () 用于在运行期间将字符串添加到字符串常量池中。使用 - XX:StringTableSize 可设置 StringTable 的长度。

  • 在 JDK6 中 StringTable 是固定的,就是 1009 的长度,所以如果常量池中的字符串过多就会导致效率下降很快,StringTablesize 设置没有要求。

  • 在 JDK7 中,StringTable 的长度默认值是 60013,StringTablesize 设置没有要求。

  • 在 JDK8 中,StringTable 的长度默认值是 60013,StringTable 可以设置的最小值为 1009。

  • 通过下面一段代码验证一下 intern 的性能损耗。

/**
 * 先执行这个代码,生成一个 word.txt
 * 产生 10 万个长度不超过 10 的字符串,包含 a-z,A-Z
 */
public class GenerateString {
    public static void main(String[] args) throws IOException {
        FileWriter fw = new FileWriter("words.txt");

        for (int i = 0; i < 100000; i++) {
            //1 - 10
            int length = (int) (Math.random() * (10 - 1 + 1) + 1);
            fw.write(getString(length) + "\n");
        }

        fw.close();
    }

    public static String getString(int length) {
        String str = "";
        for (int i = 0; i < length; i++) {
            //65 - 90, 97-122
            int num = (int) (Math.random() * (90 - 65 + 1) + 65) + (int) (Math.random() * 2) * 32;
            str += (char) num;
        }
        return str;
    }
}
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
26
27
/**
 *  性能测试
 */
public class StringTest2 {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("words.txt"));
            long start = System.currentTimeMillis();
            String data;
            while ((data = br.readLine()) != null) {
                data.intern(); //如果字符串常量池中没有对应 data 的字符串的话,则在常量池中生成
            }
            long end = System.currentTimeMillis();
            System.out.println("花费的时间为:" + (end - start));//1009:218ms  100009:54ms
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
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
26
27
28

# String 的内存分配

  • 在 Java 语言中有 8 种基本数据类型和一种比较特殊的类型 String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
  • 常量池就类似一个 Java 系统级别提供的缓存。8 种基本数据类型的常量池都是系统协调的,String 类型的常量池比较特殊。它的主要使用方法有两种。
    • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。比如: String info="atguigu.com";
    • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern () 方法。
  • Java 6 及之前,字符串常量池是运行时常量池的一部分,都是放在永久代。
  • Java 7 中对字符串池的逻辑做了很大变动,把字符串常量池放到了堆中,而运行时常量池还在永久代。
    • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
    • 字符串常量池概念原本使用得比较多,在 Java 6 及其之前的版本中,由于字符串常量池在方法区,如果不考虑好,过度使用 String.intern() 可能会导致方法区的内存溢出,所以在这些版本中必须谨慎使用。但是这个改动使得我们有足够的理由让我们重新考虑在 Java 7 中使用 String.intern ()。
  • Java 8 依旧把字符串常量池放到了堆中,而运行时常量池在元空间。

字符串常量池为什么要调整?

官方说明 (opens new window)

  • 为什么要调整位置?
    • 永久代的默认空间大小比较小。
    • 永久代垃圾回收频率低,大量的字符串无法及时回收,容易进行 Full GC 产生 STW 或者容易产生 OOM:PermGen Space。
    • 堆中空间足够大,字符串可被及时回收。
  • 在 JDK 7 中,interned 字符串不再在 Java 堆的永久代中分配,而是在 Java 堆的主要部分(称为年轻代和年老代)中分配,与应用程序创建的其他对象一起分配。
  • 此更改将导致驻留在主 Java 堆中的数据更多,驻留在永久生成中的数据更少,因此可能需要调整堆大小。
  • 用代码演示,字符串常量池的调整。
/**
 * jdk6 中:
 * -XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m
 *
 * jdk8 中:
 * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 */
public class StringTest3 {
    public static void main(String[] args) {
        //使用 Set 保持着常量池引用,避免 full gc 回收常量池行为
        Set<String> set = new HashSet<String>();
        //在 short 可以取值的范围内足以让 6MB 的 PermSize 或 heap 产生 OOM 了。
        short i = 0;
        while(true){
            set.add(String.valueOf(i++).intern());
        }
    }
}

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space //异常是堆内存溢出
	at java.util.HashMap.resize(HashMap.java:703)
	at java.util.HashMap.putVal(HashMap.java:662)
	at java.util.HashMap.put(HashMap.java:611)
	at java.util.HashSet.add(HashSet.java:219)
	at com.atguigu.java.StringTest3.main(StringTest3.java:22)

Process finished with exit code 1
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
26
27

# String 的基本操作

检验字符串常量池的存在

public class StringTest4 {
    public static void main(String[] args) {
        System.out.println();//2135
        System.out.println("1");//2136
        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");//2146
        //如下的字符串"1" 到 "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");//2146
    }
}
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
26

在代码中打上几个断点,然后根据 DEBUG,记得要把 DEBUG 页面中的 memory 选项勾上。然后可以看到执行到第二段 123... 的时候计数已经不会增长了。说明这些数据已经在字符串常量池中。

官方的例子

public class Memory {
    public static void main(String[] args) {//line 1
        int i = 1;//line 2
        Object obj = new Object();//line 3
        Memory mem = new Memory();//line 4
        mem.foo(obj);//line 5
    }//line 9

    private void foo(Object param) {//line 6
        String str = param.toString();//line 7
        System.out.println(str);
    }//line 8
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在 param 调用 toString 后就在字符串常量池创建了一个地址。

# 字符串拼接操作

  • 常量与常量的拼接结果在常量池,原理是编译期优化。

    点击查看
    @Test
    public void test1() {
        String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
        String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给 s2
        /*
         * 最终.java 编译成.class,再执行.class
         * String s1 = "abc";
         * String s2 = "abc"
         */
        System.out.println(s1 == s2); //true
        System.out.println(s1.equals(s2)); //true
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    直接看字节码

    0 ldc #2 <abc>
    2 astore_1
    3 ldc #2 <abc>
    5 astore_2
    // ldc 就是把字符串加载到运行时常量池,两个都是 abc,所以都是 true。
    
    1
    2
    3
    4
    5
  • 常量池中不会存在相同内容的变量。

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

    点击查看
    @Test
    public void test2() {
        String s1 = "javaEE";
        String s2 = "hadoop";
    
        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";//编译期优化
        //如果拼接符号的前后出现了变量,则相当于在堆空间中 new String(),具体的内容为拼接的结果:javaEEhadoop
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2; // 在 jdk5.0 之前用的是 StringBuffer 在 5.0 之后用的 StringBuilder
    
        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,并返回次对象的地址。
        String s8 = s6.intern();
        System.out.println(s3 == s8);//true
    }
    // 从字节码的角度来看,从 s5 开始,使用的是 StringBuilder 对象
    ...............
     13 new #9 <java/lang/StringBuilder>
     16 dup
     17 invokespecial #10 <java/lang/StringBuilder.<init> : ()V>
     20 aload_1
     21 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
     24 ldc #7 <hadoop>
     26 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
     29 invokevirtual #12 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
     32 astore 5
    ...............
    
    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
  • 如果左右两边都是字符串常量或者常量引用,仍然是编译期优化,而非 StringBuilder。

    点击查看
    public void test4() {
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);//true
    }
    
    //字节码文件,可以看到还是用的常量
    //针对于 final 修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上 final 的时候建议使用上
     0 ldc #14 <a>
     2 astore_1
     3 ldc #15 <b>
     5 astore_2
     6 ldc #16 <ab>
     8 astore_3
     9 ldc #16 <ab>
     11 astore 4
    ..............
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  • 使用 StringBuilder 拼接的效率要远远高于 String 拼接。

    点击查看
    //在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值 highLevel 的情况下,
    //建议使用构造器实例化:StringBuilder s = new StringBuilder(highLevel); //new char[highLevel] 这样可以避免频繁扩容。
    @Test
    public void test6() {
        long start = System.currentTimeMillis();
        //method1(100000);//4574ms
        method2(100000);//8ms
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:" + (end - start));
    }
    //使用 String 的方式,前面的代码字节码中也看过了使用 String 去做拼接,实际上是创建了个 StringBuilder 对象,最后会导致内存中存在过多的 String 和 StringBuilder 结果变慢,如果 GC 了还会更慢
    public void method1(int highLevel) {
        String src = "";
        for (int i = 0; i < highLevel; i++) {
            src = src + "a";//每次循环都会创建一个 StringBuilder、String
        }
    }
    //使用 StringBuilder 的 append 方法,只会创建一个 StringBuilder 对象。
    public void method2(int highLevel) {
        //只需要创建一个 StringBuilder
        StringBuilder src = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            src.append("a");
        }
        //        System.out.println(src);
    }
    
    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
    26
  • 如果拼接的结果调用 intern () 方法,根据该字符串是否在常量池中存在,分为

    • 如果存在,则返回字符串在常量池中的地址。
    • 如果字符串常量池中不存在该字符串,则在常量池中创建一份,并返回此对象的地址。

# intern () 的使用

intern () 说明

public native String intern();
1
  • 这是一个本地方法,调用的是 C 的代码。

  • 字符串池最初是空的,由 String 类私有维护。
    当调用 intern 方法时,如果池中已包含由 equals (Object) 方法确定的等于此 String 对象的字符串,则返回池中的字符串。否则,将此 String 对象添加到池中,并返回对此 String 对象的引用。
    由此可见,对于任意两个字符串 s 和 t ,当且仅当 s. equals (t) 为 true 时 s. intern () == t. intern () 为 true。

    点击查看
    String myInfo1 = new String("I love atguigu").intern();
    String myInfo2 = new String("I love atguigu");
    myInfo2.intern();
    System.out.println(myInfo1 == myInfo2); // 这个结果为 false,因为 myInfo2.intern();的引用被忽略了。
    
    1
    2
    3
    4
  • 也就是说,如果在任意字符串上调用 String.intern (),那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是 true。

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

new String () 说明

/**
 * 题目:
 * new String("ab")会创建几个对象?
 * 1. 字符串字面量"ab",它会在字符串常量池中创建一个对象;
 * 2. 通过 new 关键字,会在堆内存中另外创建一个 String 对象 s1,这个对象的值是一个对字符串常量池中"ab"的引用。
 *
 */
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("ab");
    }
}
// 字节码文件
 0 new #2 <java/lang/String>			//在堆中创建了 String 对象
 3 dup
 4 ldc #3 <ab>					  // 在字符串常量池中创建了个对象
 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
 9 astore_1
10 return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

new String () + new String () 说明

/**
 * 题目:
 * new String("a") + new String("b")呢?
 */
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");
    }
}
1
2
3
4
5
6
7
8
9
  • 首先创建了两个对象 new String ("a") 和 new String ("b")。
  • 连接符 + 会创建一个对象 StringBuilder。
  • 在常量池创建两个对象 a 和 b。
  • 最后 str 指向的是 StringBuilder 的 toString () 创建出的一个对象 new String ("ab")。

但是不会在字符串常量池中创建 "ab"。这是因为通过连接操作生成的字符串对象是动态创建的,而不是在编译时就确定的常量。

关于 intern 的面试题

/**
 * 如何保证变量 s 指向的是字符串常量池中的数据呢?
 * 有两种方式:
 * 方式一: String s = "shkstart";//字面量定义的方式
 * 方式二: 调用 intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern();
 */
public class StringIntern {
    public static void main(String[] args) {
        String s = new String("1");
        s.intern();
        String s2 = "1";
        System.out.println(s == s2);//jdk6:false   jdk7/8:false


        String s3 = new String("1") + new String("1"); 
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
点击查看
  1. 首先是 String s = new String("1"); 会创建两个对象,一个是 new String ("1"),另一个是在字符串常量池中的 "1"。 s.intern(); 调用,返回的是字符串常量池中的 "1",s2 返回的也是字符串常量池中的 "1",但是 s 指向的却是堆中 new String ("1") 的地址。所以结果为 false。
  2. String s3 = new String("1") + new String("1"); 创建了 6 个对象,但是在字符串常量池中没有创建 "11",接下来调用 s3.intern(); 会在字符串常量池中创建 "11" 对象,但是 jdk6 因为字符串常量池在永久代中,而 jdk7 之后把字符串常量池放到了堆中即字符串常量中的存储的是堆中 new String ("11") 的地址。这样做的目的是为了节省空间。所以 jdk6 是 false,而 jdk7 之后为 true。

关于 intern 的一些面试题拓展

public class StringExer1 {
    public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");//new String("ab")
        //在上一行代码执行完以后,字符串常量池中并没有"ab"

        String s2 = s.intern();//jdk6 中:在串池中创建一个字符串"ab"
        //jdk8 中:串池中没有创建字符串"ab",而是创建一个引用,指向 new String("ab"),将此引用返回

        System.out.println(s2 == "ab");//jdk6:true  jdk8:true
        System.out.println(s == "ab");//jdk6:false  jdk8:true
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
点击查看

String s = new String("a") + new String("b"); 创建了 6 个对象, String s2 = s.intern(); 在字符串常量池创建一个对象,然后赋值给 s2。

  • jdk6:s2 和 "ab" 就都指向的是字符串常量池中的对象地址,所以 s2 和 ab 相等。而 s 指向的是堆中的 new String ("ab"),所以 s 和 "ab" 不同。
  • jdk7:因为字符串常量池移到了堆中,字符串常量池中实际放的是堆的地址,所以 s 和 "ab" 的地址相同。
//这个解释就是和之前的解释一样了
public class StringExer2 {
    public static void main(String[] args) {
//        String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"
        String s1 = new String("a") + new String("b");////执行完以后,不会在字符串常量池中会生成"ab"
        s1.intern();
        String s2 = "ab";
        System.out.println(s1 == s2);
    }
}
1
2
3
4
5
6
7
8
9
10

intern () 的效率测试(空间角度)

/**
 * 使用 intern()测试执行效率:空间使用上
 *
 * 结论:对于程序中大量存在存在的字符串,尤其其中存在很多重复字符串时,使用 intern()可以节省内存空间。
 */
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 (InterruptedException e) {
            e.printStackTrace();
        }
        System.gc();
    }
}
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
26
27
28
点击查看

通过查看 JProfiler 或者 JVisualvm 可以看到,如果在使用大量字符串的前提下,使用 intern 方法可以节省大量的内存空间。主要是因为一旦用了 intern 那么就是在字符串常量池中创建,这样就不需要维护堆内存中的实例对象。

总结 intern ()

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

# StringTable 的垃圾回收

/**
 * 通过修改循环的次数来看垃圾回收。
 * String 的垃圾回收:
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 */
public class StringGCTest {
    public static void main(String[] args) {
        for (int j = 0; j < 100000/**10/1000**/; j++) {
            String.valueOf(j).intern();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

在循环为 10 的时候,没有发生 GC,并且在 StringTable statistics 中也可以看到,字符串常量池中的数据量、大小。

在将循环改成 10 万次后,在年轻代发生了 GC。至于说这个显示的 60013 不足 10 万条数据,并非是将那 4 万条数据给清空了,而是通过哈希到了同一个桶,形成了链表。

# G1 中的 String 去重操作

官方描述:JEP 192: String Deduplication in G1 (openjdk.org) (opens new window)

注意不是字符串常量池的去重操作,字符串常量池本身就没有重复的。

背景

  • 对许多 Java 应用(有大的也有小的)做的测试得出以下结果:
    • 堆存活数据集合里面 String 对象占了 25%。
    • 堆存活数据集合里面重复的 String 对象有 13.5%。
    • String 对象的平均长度是 45。
  • 目前,许多大型 Java 应用程序都存在内存瓶颈。测量结果表明,在这类应用程序中,Java 堆实时数据集约有 25% 被 String 对象占用。此外,在这些 String 对象中,大约有一半是重复的,重复意味着 string1.equals(string2) 属实。堆上有重复的 String 对象,从本质上讲就是浪费内存。本项目将在 G1 垃圾收集器中实现自动和持续的 String 重复数据删除,以避免浪费内存并减少内存占用。

实现

  • 标记阶段:在 G1 的并发标记阶段,会扫描整个堆,找出所有的字符串对象。
  • 去重阶段:在标记阶段结束后,会启动一个名为 StringDeduplicationTable 的哈希表,用于存储所有唯一的字符串。然后,G1 会启动一个后台线程(StringDedupThread),这个线程会遍历所有的字符串对象,对每个字符串对象,都会检查 StringDeduplicationTable 中是否已经存在相同的字符串。如果存在,就将当前字符串对象的引用指向 StringDeduplicationTable 中的字符串;如果不存在,就将当前字符串添加到 StringDeduplicationTable 中。
  • 清理阶段:在下一次 G1 的并发标记阶段,会清理掉所有不再使用的字符串。

命令

  • -XX:+UseG1GC:使用 G1 垃圾回收器。
  • -XX:+UseStringDeduplication:开启 String 去重,默认是不开启的,需要手动开启。
  • -XX:+PrintStringDeduplicationStatistics:打印详细的去重统计信息。
  • -XX:StringDeduplicationAgeThreshold=2:一个对象只需要经历 2 次垃圾收集,就有可能被考虑进行去重操作。
#JVM
上次更新: 2025/04/12, 07:54:33
执行引擎
垃圾回收概述

← 执行引擎 垃圾回收概述→

最近更新
01
Reactor 核心
02-24
02
前置条件
10-30
03
计算机网络
09-13
更多文章>
Theme by Vdoing | Copyright © 2019-2025 powered by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式