String对象和String常量池

1. String的基本特性

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

1.1 为什么String在jdk9 之后改变了其底层结构

官方解释: http://openjdk.java.net/jeps/254

  1. String类的jdk8之前的实现将字符存储在char数组中,每个字符使用两个字节(16位)。

  2. 但是从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且大多数字符串对象只包含拉丁字符。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用。 例如 存ab 这个字符,如果使用char 数组,就要分配两个字符的空间,即 四个字节, 但是ab 作为英文,本身只需占用两个字节即可

  3. 之前 String 类使用 UTF-16 的 char[] 数组存储,现在改为 byte[] 数组 外加一个编码标志位存储,该编码标志将指定 String 类中 byte[] 数组的编码方式

结论:String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间 ,

同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改

1.2 String 的不可变性

  1. 当对字符串变量重新赋值时,会直接新建一个字符串(或池中本来就有的),不会影响本来的字符串
  2. 当对现有的字符串进行连接拼接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  3. 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

2. String的内存分配位置

在Java语言中有8种基本数据类型和一种比较特殊的非常常用的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。

常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。比如:String info="atguigu.com";
  • 如果不是用双引号声明的String对象(new出来的,或者其他方法返回的),可以使用String提供的intern()方法。

代码演示:

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
}

示意图:1605876490266

如上图所示,, 堆中的Object 对象在调用toString 方法后,将在String pool 中生成一个字符串对象,并返回给 顶层栈帧foo方法中的局部变量 str

String 内存分配的演进过程

  1. Java 6及以前,字符串常量池存放在永久代
  2. Java 7中 Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内
    • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
    • 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()

JDK6 : 1605875141440

JDK7:

1605875161992

为什么要调整String 常量池的位置呢

  • 永久代的默认比较小
  • 永久代垃圾回收频率低
  • 堆中空间足够大,字符串可被及时回收

3 String 常量池的底层结构

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

  1. String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern()方法时性能会大幅下降。
  2. 使用-XX:StringTablesize可设置StringTable的长度
  3. 在JDK6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快,StringTablesize设置没有要求
  4. 在JDK7中,StringTable的长度默认值是60013,StringTablesize设置没有要求
  5. 在JDK8中,StringTable的长度默认值也是60013,但是限定了StringTable可以设置的最小值为1009

字符串常量池中同一个字符串只存在一份

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

代码示例:

public class StringTest4 {
    public static void main(String[] args) {
        System.out.println();//2330
        System.out.println("1");//2331 个字符串
        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");//2340 个字符串

        //如下的字符串"1" 到 "10"不会再次加载
        System.out.println("1");//2341 
        System.out.println("2");//2341
        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");//2341
    }
}

在第一波开始,堆中的String 个数:

1605876129330

第一波结束时, 字符串个数,加了十个:

1605876173836

之后就再没增加过了:

1605876215302

测试不同的 StringTable长度下,程序的性能

首先先创建一个拥有10W行不同字符的文件,程序自行编写,这里就不演示了

/**
 * -XX:StringTableSize=1009
 */
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的字符串的话,则在常量池中生成
                data.intern();
            }

            long end = System.currentTimeMillis();

            System.out.println("花费的时间为:" + (end - start));//1009:143ms  100009:47ms
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}

  • -XX:StringTableSize=1009 :程序耗时 143ms
  • -XX:StringTableSize=100009 :程序耗时 47ms

4. 字符串的拼接操作

  1. 常量与常量的拼接结果在常量池,在编译期就进行运算了
  2. 常量池中不会存在相同内容的变量
  3. 拼接前后,只要其中有一个是变量,就会就在堆中new一个。不在常量池中,变量拼接的原理是StringBuilder
  4. 如果是new出来的String调用intern()方法,则会判断常量池中有没有这个字符
    • 如果存在,则返回字符串在常量池中的地址
    • 如果字符串常量池中不存在该字符串,则在常量池中创建一份,并返回此对象的地址

4.1 两个代码演示

代码一:

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
}

结果打印的都是 true,不管是 地址值还是 内容 都一样,看下面解析出的字节码指令,在指令0的位置从常量池中直接加载"abc"

 0 ldc #2 <abc>
 2 astore_1
 3 ldc #2 <abc>
 5 astore_2
 6 getstatic #3 <java/lang/System.out>
 9 aload_1
10 aload_2
11 if_acmpne 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #4 <java/io/PrintStream.println>
22 getstatic #3 <java/lang/System.out>
25 aload_1
26 aload_2
27 invokevirtual #5 <java/lang/String.equals>
30 invokevirtual #4 <java/io/PrintStream.println>
33 return

代码二:

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;

    System.out.println(s3 == s4);//true
    //后面都是false 的原因是,只要拼接时 有变量参与,都是相当于new 一个 不从常量池中共享
    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();
    //s6 虽然是堆中new出来的,但是调用intern方法返回出的是 常量池中的,所以和 s3 地址一样
    System.out.println(s3 == s8);//true
}

4.2 字符串变量拼接的底层实现

前面说到, 一旦拼接操作有变量引用的参与,而不是全部都是字面量的形式, 就会 在堆中 新new一个对象,这是为什么呢?,下面用一个简单的代码解释

代码:

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

字节码指令:

0 ldc #14 <a>  //将字符a 从字符串常量池中获取
 2 astore_1  //放入 局部变量索引为 1 的位置  (0为this)
 3 ldc #15 <b>  //将字符b从字符串常量池中获取
 5 astore_2  //放入 局部变量索引为 2 的位置
 6 ldc #16 <ab>  //将字符ab 从字符串常量池中获取
 8 astore_3   //放入 局部变量索引为 3 的位置
 9 new #9 <java/lang/StringBuilder>  // new 一个 StringBuilder对象,开辟空间
12 dup
13 invokespecial #10 <java/lang/StringBuilder.<init>>  //初始化该StringBuilder对象
16 aload_1  // 加载 局部变量表中索引为1的值 ,也就是 a
17 invokevirtual #11 <java/lang/StringBuilder.append>  //调用 StringBuilder对象 的 append 方法
20 aload_2  //加载 b 
21 invokevirtual #11 <java/lang/StringBuilder.append> //同样append
24 invokevirtual #12 <java/lang/StringBuilder.toString> //最后调用StringBuilder对象的toString方法
27 astore 4  //放入 局部变量表 索引为 4的位置
29 getstatic #3 <java/lang/System.out>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #4 <java/io/PrintStream.println>
46 return

从上面的 字节码 逐行解释中看, 带有 变量的字符串拼接,其底层是使用 StringBuilder 对象

相当于如下代码:

StringBuilder s = new StringBuilder();
s.append("a")
s.append("b")
s.toString()  //  约等于 new String("ab"),方法内就是 new String ,但是有些不同 后面说

补充:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer

是不是所有字符串变量的拼接操作都是使用的是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
}

字节码: 看 指令9的位置,为 s1+s2 的操作,并没有使用 拼接的方式,而是在编译器就已经处理好了

 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
13 getstatic #3 <java/lang/System.out>
16 aload_3
17 aload 4
19 if_acmpne 26 (+7)
22 iconst_1
23 goto 27 (+4)
26 iconst_0
27 invokevirtual #4 <java/io/PrintStream.println>
30 return

结论: 和直接使用字面量相同, 如果拼接字符变量都为 final ,都是可以在编译器就可以确定结果的, 所以也被编译器优化了(在写代码时,可以写final的都可以写上,优化代码)

4.3 拼接操作和使用StringBuilder拼接的性能差距

代码

public void test6(){

    long start = System.currentTimeMillis();

    //method1(100000);//4014
    method2(100000);//7

    long end = System.currentTimeMillis();

    System.out.println("花费的时间为:" + (end - start));
}

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

public void method2(int highLevel){
    //只需要创建一个StringBuilder
    StringBuilder src = new StringBuilder();
    for (int i = 0; i < highLevel; i++) {
        src.append("a");
    }
}

我们发现, 使用method1方法,拼接字符串10w次, 使用时间为 4014ms,

而使用method2方法,append 方式,仅仅只需7ms, 差距如此之大

为什么?

  • StringBuilder的append()的方式:自始至终中只创建过一个StringBuilder的对象,并在操作结束后返回一个字符串,操作过程中不会产生
  • 而 使用String的字符串拼接方式:每一次拼接操作都会创建一个StringBuilder和String的对象,也就是20w个对象, 期间触发GC的可能也很大, 进一步变慢

对于上面的StringBuilder方式,还有更一步的改进方式

查看 StringBuilder 类底层的实现时,发现初始定义一个 char 型数组(JDK8),用于append操作, 在数组长度不够时,会创建一个更长的,并进行拷贝, 这也有点浪费时间,所以在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化:

StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]

5. String的intern方法

5.1 intern()方法的基本说明

前面已经有使用过,并且大概解释过,现在具体说明一下

public native String intern();
  1. intern是一个native方法,调用的是底层C的方法

  2. 字符串池最初是空的。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串对象相等的字符串(也就是值相等),则返回池中的字符串。否则,该字符串对象值放一个到池中,并返回对该字符串对象的引用。

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

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

  4. 通俗点讲,String就是确保字符串在常量池里有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。

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

方式一 : String s = "Hello"; //直接使用字面量

方式二: 使用 intern方法

  • String s = new String("Hello").intern()'
  • String s = new StringBuilder("123").toString().intern();

5.2 new String("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>  //从 常量池中获取 ab 字符串对象,如果没有 则会创建
 6 invokespecial #4 <java/lang/String.<init>>  //使用 ab 字面量去初始化 堆中的 String 对象
 9 astore_1
10 return

所以有上面的结论 ,一个或两个, 如果常量池中没有这个字面量的情况是是会创建两个的

看看 String类的带参构造函数

private final char[] value;
private int hash;
//...
public String(String var1) {
  this.value = var1.value;
   this.hash = var1.hash;
}

可以看到,将常量池String对象中的value 属性,也就是维护的char[] 和字符的hash值赋给了 new String 对象的 value属性和 hash属性,所以 new 出的String 对象的内容为参数中的值, 这也说明了,虽然 常量池中的String对象 和new 出的 String 对象本身地址值不同,但是他们所维护的char[]却是同一个

那么new String(“a”) + new String(“b”) 会创建几个对象?

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

字节码指令:

 0 new #2 <java/lang/StringBuilder> // 1. StringBuilder 对象
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init>>
 7 new #4 <java/lang/String> // 2. new String("a") 对象
10 dup
11 ldc #5 <a>  //3. 常量池中 a 字符串对象
13 invokespecial #6 <java/lang/String.<init>> 
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String> // 4. new String 对象
22 dup
23 ldc #8 <b>  //5. 常量池中 b 字符串对象
25 invokespecial #6 <java/lang/String.<init>>  
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>  //toString 返回的
34 astore_1
35 return
     

所有上面的代码 最多创建 6个对象

  • 对象1:new StringBuilder()
  • 对象2: new String("a")
  • 对象3: 常量池中的"a"
  • 对象4: new String("b")
  • 对象5: 常量池中的"b"
  • 对象6: toString方法创建的对象

深入剖析: StringBuilder的toString():

代码:

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

这是StringBuilder 对象的toString 方法, 将 StringBuilder 类中append 处理的char[] 连接为一个字符串,

和普通的 new String("ab")不一样, 并没有使用字面量的形式创建,所以没有在 常量池中创建"ab" 字符串

所以这里就创建了一个

5.3 JDK7 前后 intern() 方法的变化

在前面说到, intern() 方法是将判断调用者字符串对象的值 是否在常量池中存在,如果存在 则返回常量池中的那个对象引用,如果不存在则 造一个, 但是这个造一个 ,在随着JDK7 将字符串常量池移到堆空间时,发生了变化

代码示例:

public class StringIntern {
   public static void main(String[] args) {
       String s = new String("1");
       s.intern();//此方法调用前常量池中就已经有 字符串 "1"了
       String s2 = "1";
       System.out.println(s == s2);  //jdk6:false   jdk7/8:false
       
     
       // 执行完下一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
       String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
       s3.intern(); 
       String s4 = "11";
       System.out.println(s3 == s4);// jdk6:false  jdk7/8:true
   }
}

上面的代码中

  • 第一个输出语句 无论如何都是打印false,那是因为在创建 new String("1")时已经在堆中创建了 字符串 "1",所以intern() 方法没有再创建,而S2则引用了常量池中的对象,所以和 new String()s 一直都是false

  • 第二个输出语句 在jdk6 中打印了 false, 是因为 代码 new String("1") + new String("1")相当于 创建了一个new String("11") 赋予了 s3 ,但是根据上一节说到的,并没有在常量池中生成 "11",所以 当调用intern() 方法时, 创建了一个新的字符串 "11" 放到了常量池中,注意 此时的环境为JDK6, 字符串常量池在 方法区中,和new的对象不在一起,所以是创建一个新的方式

  • 但是在JDK7 及以后的版本,却是打印 true,是因为 字符串常量挪到了 堆中,和 new的对象在一起,所以杜绝空间浪费, 调用intern() 方法创建对象时,没有拷贝新的,而是存了 new String() 对象的引用到字符串常量池中,所以打印为 true

内存示意图:

JDK6:

1605972768473

JDK7

1605972800862

扩展

public class StringIntern1 {
    public static void main(String[] args) {
        String s3 = new String("1") + new String("1");//new String("11")
        //在字符串常量池中生成对象"11"
        String s4 = "11";
        String s5 = s3.intern();

        System.out.println(s3 == s4);//false

        System.out.println(s5 == s4);//true
    }
}

将上面的代码中 声明字面量提到调用intern() 方法前后,打印结果就固定了,那是因为在声明"11" 时已经创建一个新的字符串到常量池中. 所以必定是false;

5.4 intern方法的总结

当调用 intern 方法时:

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

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

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

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

5.5 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();
        }
    }
}

上面的代码中,将 1-10 每次使用 String.valueOf 的方式转换为 字符串 存入数组中,循环1000W次, 下面看 使用intern方法和不使用的区别

不使用: valueOf 方法中每次都在堆中new 了一个新的字符串对象, 所以共创建了 1000w个对象

1605976733817

使用intern: 虽然每次也都创建了 String 对象, 但是最后返回的 却是intern 方法返回的 字符串常量池中的,会被重用, 而因为数组中并没有指向在堆中创建的String 对象,将在垃圾回收时 被销毁,减少内存,查看内存数据也是如此

1605977024929

结论:

  1. 对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省内存空间。
  2. 大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用intern() 方法,就会很明显降低内存的大小。

6 String 的垃圾回收

代码演示: 创建10w个字符串

public class StringGCTest {
    public static void main(String[] args) {
        for (int j = 0; j < 100000; j++) {
            String.valueOf(j).intern();
        }
    }
}

jvm 参数: -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails 打印 字符串常量池中的信息和垃圾回收的信息

打印信息如下, 很明显在新生代PSYoungGen发生了垃圾回收的行为, 并且看出 字符串常量池中也不足10w个对象

1606031843868

打印内容:

Heap
 PSYoungGen      total 4608K, used 3883K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 82% used [0x00000000ffb00000,0x00000000ffe50fb0,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 11264K, used 228K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 11264K, 2% used [0x00000000ff000000,0x00000000ff039010,0x00000000ffb00000)
 Metaspace       used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     14158 =    339792 bytes, avg  24.000
Number of literals      :     14158 =    603200 bytes, avg  42.605
Total footprint         :           =   1103080 bytes
Average bucket size     :     0.708
Variance of bucket size :     0.711
Std. dev. of bucket size:     0.843
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :     62943 =   1510632 bytes, avg  24.000
Number of literals      :     62943 =   3584040 bytes, avg  56.941
Total footprint         :           =   5574776 bytes
Average bucket size     :     1.049
Variance of bucket size :     0.824
Std. dev. of bucket size:     0.908
Maximum bucket size     :         5

补充: G1中 String 去重操作

这个去重操作针对的不是 String本身,因为 String 常量池中 本身就不是重复的,而是针对 new String() 中维护的char[]

String 去重操作的背景

  1. 对许多Java应用(有大的也有小的)做的测试得出以下结果:

    • 堆存活数据集合里面String对象占了25%
    • 堆存活数据集合里面重复的String对象有13.5%
    • String对象的平均长度是45
  2. 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半String对象是重复的,重复的意思是说:

  3. str1.equals(str2)= true。堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。

String 去重的的具体实现

  1. 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
  2. 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
  3. 使用一个Hashtable来记录所有的被String对象(堆中创建的 和 常量池中的)使用的不重复的char数组。当去重的时候,会查这个Hashtable,来看堆上是否已经存在一个一模一样的char数组。
  4. 如果存在,String对象会被调整内部维护的数组去,引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
  5. 如果查找失败,char数组会被插入到Hashtable,这样以后的时候就可以共享这个数组了。

命令行选项:

  1. UseStringDeduplication(bool) :开启String去重,默认是不开启的,需要手动开启。
  2. PrintStringDeduplicationStatistics(bool) :打印详细的去重统计信息
  3. stringDeduplicationAgeThreshold(uintx) :达到这个年龄的String对象被认为是去重的候选对象
原文地址:https://www.cnblogs.com/xjwhaha/p/14101079.html