zoukankan      html  css  js  c++  java
  • 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对象被认为是去重的候选对象
  • 相关阅读:
    Js--小笔记
    Android Gson解析
    java格式化数字、货币、金钱
    关于Edittext默认弹出软键盘为数字键
    生日星座自动匹配
    ANDROID STUDIO系列教程六--GRADLE多渠道打包
    框架,简化了代码的同时,也让我们慢慢变蠢
    Android开发实现高德地图定位
    onNewIntent调用时机
    EditText输入手机号自动带空格
  • 原文地址:https://www.cnblogs.com/xjwhaha/p/14101079.html
Copyright © 2011-2022 走看看