关于java:我所知道JVM虚拟机之Spring-Table字符串常量池

5次阅读

共计 14675 个字符,预计需要花费 37 分钟才能阅读完成。

前言

咱们在理论开发当中应用 String 十分的宽泛,那么对应用 String 类其实有很多角度能够去学习了解

那么本篇文章,咱们从应用 String 的档次到开始理解剖析 String 的实现、性能等等

一、String 的根本个性


对于 String 咱们称为字符串,应用 一对“”引号 起来示意

那么平时咱们的应用有不同的定义形式如下:

  • String s1 = "xiaomingtongxue" ; 称说为字面量的定义形式
  • String s2 = new String("hello");称说为对象的形式

咱们能够察看一下 String 的源代码剖析看看

如图能够察看到 String 被申明为 final 的:示意它不可被继承

如图能够察看到 String 实现了 Serializable 接口:示意字符串是反对序列化的

如图能够察看到实现了 Comparable 接口:示意 String 能够比拟大小

如图能够察看到 String 在 jdk8 及以前外部定义了 final char value[] 用于存储字符串数据

然而它在 JDK9 时改为了 byte[],咱们能够切换到 JDK9 的环境去看看 String 的源码

为什么 JDK9 扭转了 String 的构造

================================

能够拜访官网文档查看具体的阐明:拜访入口

具体咱们就粘贴官网的阐明进行翻译解释

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

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

对于 String 来说它代表了不可变的字符序列,简称:不可变性。

接下来咱们应用示例领会一下它的不可变性

public void test2() {
   String s1 = "abc";
   String s2 = "abc";
   System.out.println(s1 == s2);
   System.out.println(s1);
   System.out.println(s2);
}    
// 运行如下:true
abc
abc

论断:这两个 abc 理论共用的是堆空间里的字符串常量池里边的同一个,所以两个援用地址是一样的

public void test2() {
   String s1 = "abc";
   String s2 = "abc";
   s1 = "hello";
   System.out.println(s1 == s2);
   System.out.println(s1);
   System.out.println(s2);
}
// 运行如下:false
abc
abc

论断:当对 s1 字符串从新赋值时会新建一个指定内存区域并赋值,所以在比拟的时候两个援用地址不一样

public void test2() {
   String s1 = "abc";
   String s2 = "abc";
   s2 += "def";
   System.out.println(s2);
   System.out.println(s1);
}
// 运行如下:abcdef
abc

论断:当对 s1 字符串进行连贯操作时也会新建一个指定内存区域并赋值,不在原有的 value 进行赋值

public void test3() {
    String s1 = "abc";
    String s2 = s1.replace('a', 'm');
    System.out.println(s1);
    System.out.println(s2);
}
// 运行如下:abc
mbc

论断:当调用 string 的 replace()操作时也会新建一个指定内存区域并赋值,不在原有的 value 进行赋值

一道口试题

================================

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);
        System.out.println(ex.ch);
    }
}

那么当咱们运行起来的时候,会输入什么呢?输入:good、best

原 str 的援用地址的内容并没有变,办法里的 str =“test ok”,其实是字符串常量池中的另一个区域(地址),将它进行赋值操作给 str 但并没有批改原来 str 指向的援用地址里的内容

论断:字符串常量池中是不会存储雷同内容的字符串的

字符串常量池怎么保障不会存储雷同内容的?

================================

因为 String Pool(字符串常量池)是一个固定大小的 Hashtable,默认值大小长度是 1009

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

JDK7 中,StringTable 的长度 默认值是 60013

JDK8 中,StringTable 的长度 默认值是 60013,StringTable 能够设置的 最小值为 1009

如果放进 String Pool 的 String 十分多就会造成 Hash 抵触重大,从而导致链表会很长而链表长了后间接会造成的影响就是当调用 String.intern()办法时性能会大幅降落

所以裁减 StringTable 的长度,应用 -XX:StringTablesize 可设置 StringTable 的长度

咱们应用一个示例来领会一下不同版本的默认长度是多少

public static void main(String[] args) {
    // 测试 StringTablesize 参数
    System.out.println("我来打个酱油");
    try {Thread.sleep(1000000);
    }catch (InterruptedException e){e. printStackTrace();
    }
}
JDK 6 环境下的大小设置:

这时将我的项目跑起来,在关上 cmd 命令窗口查看一下是否已被批改为:10 的大小

JDK 7 环境下的大小设置:

这时将我的项目跑起来,在关上 cmd 命令窗口查看一下大小是多少

接下来咱们再测试不同大小长度的速度是怎么样的,学生成 10 万个长度不超过 10 的字符串

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;
}

接下来咱们依据设置不同大小看看,将这 10 万个长度不超过 10 的字符串读取看看效率怎么样

破费工夫:143ms

破费工夫:47ms

二、String 的内存调配


那么 String 构造次要放在哪呢?其实咱们在前几篇有提到过这个放的地位

那么咱们再出于完整性的思考并且例子来阐明的确是这样的构造当中

首先咱们说在 Java 语言中有 8 种根本数据类型 1 种比拟非凡的类型 String

那么这九种那个唯独能够放在一起呢?那就是常量池使得它们更快更节俭内存等

常量池就相似一个 Java 零碎级别提供的缓存,8 种根本数据类型的常量池都是零碎协调的,String 类型的常量池比拟非凡。它的次要应用办法有两种

  • 间接应用双引号申明进去的 String 对象会间接存储在常量池中。
  • 如果不是用双引号申明的 String 对象,能够应用 String 提供的 intern()办法。这

Java 6 及以前,字符串常量池寄存在永恒代

Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的扭转,行将字符串常量池的地位调整到 Java 堆内

所有的字符串都保留在堆(Heap)中,和其余一般对象一样,这样能够让你在进行调优利用时仅须要调整堆大小就能够了

字符串常量池概念本来应用得比拟多,然而这个改变使得咱们有足够的理由让咱们重新考虑在 Java 7 中应用 String.intern()。

Java8 元空间,字符串常量在堆

咱们能够应用示例来领会一下字符串常量池爆出 OOM 的不同状况

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());
        }
    }
}
JDK 6 环境运行下在永恒代中:

Exception in thread "main" java.lang.outOfMemoryError: PermGen space
at java.lang.string.intern(Native Method)
at com.atguigu.java.stringTest3.main(StringTest3. java:22)
JDK 8 环境运行下在堆中:

运行后果
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)

那么为什么要调整字符串常量池的地位?

================================

首先永恒代的默认空间大小比拟小并且垃圾回收频率低,大量的字符串无奈及时回收,容易进行 Full GC 产生 STW 或者容易产生 OOM:PermGen Space

而堆中空间足够大,字符串可被及时回收

在 JDK 7 中,interned 字符串不再在 Java 堆的永恒代中调配,而是在 Java 堆的次要局部(称为年老代和年轻代)中调配,与应用程序创立的其余对象一起调配

此更改将导致驻留在主 Java 堆中的数据更多,驻留在永恒生成中的数据更少,因而可能须要调整堆大小

三、String 的基本操作


咱们来看一个代码示例,依照咱们之前说的个性,前面同样的字符串则不会再生成

 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");
    // 如下的字符串 "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");
}

那么咱们应用 debug 运行起来代码,看看真的是这样吗?

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

那么接下来咱们再看一个列子,当咱们运行起来一起剖析 main 办法与 foo 办法各指向的地位

// 官网示例代码
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
}

四、字符串拼接操作


咱们先来看看以下代码的拼接操作,若进行匹配的话后果是什么呢?

public void test1(){
    String s1 = "a" + "b" + "c";
    String s2 = "abc";
    System.out.println(s1 == s2);
    System.out.println(s1.equals(s2));
}

后果是输入两个 true、为什么呢?咱们先将它进行编译一起看看.class 文件内容是什么

public void test1(){
    String s1 = "abc";
    String s2 = "abc";
    System.out.println(s1 == s2);
    System.out.println(s1.equals(s2));
}

咱们发现常量与常量的拼接的话,它们的后果在常量池原理是编译优化将 s1 等同于 ”abc”

同时咱们也能够看看该字节码是怎么回事

从字节码指令看出:编译器做了优化,将“a”+“b”+“c”优化成了“abc”

接下来在看看下一个示例代码

public void test2(){

    String s1 = "javaEE";
    String s2 = "hadoop";
    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4);
    System.out.println(s3 == s5);
    System.out.println(s3 == s6);
    System.out.println(s3 == s7);
    System.out.println(s5 == s6);
    System.out.println(s5 == s7);
    System.out.println(s6 == s7);
    
    String s8 = s6.intern();
    System.out.println(s3 == s8);
}

那么当代码运行起来后运行后果为:true、false、false、false、false、false、false、false、

那么为什么会这样呢?咱们和下面一样将它编译并且看看.class 文件内容

public void test2(){

    String s1 = "javaEE";
    String s2 = "hadoop";
    String s3 = "javaEEhadoop";
    String s4 = "javaEEhadoop";
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4);
    System.out.println(s3 == s5);
    System.out.println(s3 == s6);
    System.out.println(s3 == s7);
    System.out.println(s5 == s6);
    System.out.println(s5 == s7);
    System.out.println(s6 == s7);
    
    String s8 = s6.intern();
    System.out.println(s3 == s8);
}

咱们发现 s4 常量与常量的拼接的话,会进行编译优化就连接成一起了

如果拼接符号的前后呈现了变量则相当于在堆空间中 new String(),具体的内容为拼接的后果:javaEEhadoop(s5、s6、s7 呈现)所以此时再进行比拟的时候,后果就会为 false

那么咱们发现 s8 = s6.intern(),那么它是什么状况呢?

如果拼接的后果调用 intern()办法,依据该字符串是否在常量池中存在,分为:

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

而咱们 s6 拼接符号的前前面呈现了变量则堆空间中 new String(),所以字符串常量池不存在

接下来在看看下一个示例代码

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

那么当代码运行起来后运行后果为:false

那么为什么会这样呢?咱们一起运行剖析一下字节码看看

咱们发现 s4 = s1 + s2 的时候第九行操作指令创立 StringBuilder 并进行了实例化赋默认值

将局部变量表索引为 1 的 a、索引为 2 的 ab 进行 append。咱们能够应用长期代码展现这个操作

public void test3(){
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    /*
    如下的 s1 + s2 的执行细节:(变量 s 是我长期定义的)① StringBuilder s = new StringBuilder();
    ② s.append("a")
    ③ s.append("b")
    ④ s.toString()  --> 约等于 new String("ab"),但不等价
    补充:在 jdk5.0 之后应用的是 StringBuilder, 在 jdk5.0 之前应用的是 StringBuffer
     */
    System.out.println(s3 == s4);
}

论断:拼接前后只有其中有一个是变量后果就在堆中。变量拼接的原理是 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

那么为什么会这样呢?不是说相当于 StringBuilder 新建存储在堆中吗?让咱们一起来剖析剖析

如果拼接符号左右两边都是字符串常量或常量援用,则依然应用编译期优化,即非 StringBuilder 的形式

针对于 final 润饰类、办法、根本数据类型、援用数据类型的量的构造时,能应用上 final 的时候倡议应用上

从字节码角度来看:为变量 s4 赋值时,间接应用 #16 符号援用,即字符串常量“ab”

接下来咱们应用一个拼接操作与 append 操作的效率进行比照

public void test6(){long start = System.currentTimeMillis();
    method1(100000);//4014
    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}
}
// 运行后果如下:破费的工夫:4014
public void test6(){long start = System.currentTimeMillis();
    method2(100000);
    long end = System.currentTimeMillis();
    System.out.println("破费的工夫为:" + (end - start));
}
public void method2(int highLevel){
    // 只须要创立一个 StringBuilder
    StringBuilder src = new StringBuilder();
    for (int i = 0; i < highLevel; i++) {src.append("a");
    }
}
// 运行后果如下:破费的工夫:7

通过输入破费的工夫咱们能够领会执行效率:通过 StringBuilder 的 append()的形式增加字符串的效率要远高于应用 String 的字符串拼接形式!

起因是因为 StringBuilder 的 append()的形式:从头至尾中只创立过一个 StringBuilder 的对象

那么对于应用 String 的字符串拼接形式有有余呢:

  • 创立过多个 StringBuilder 和 String(调的 toString 办法)的对象,内存占用更大;
  • 如果进行 GC,须要破费额定的工夫(在拼接的过程中产生的一些两头字符串可能永远也用不到,会产生大量垃圾字符串)。

五、intern()的应用


咱们先看看 intern 在 String 类是什么怎么形容的呢?

咱们看图就能够晓得 intern 是一个 native 办法,调用的是底层 C 的办法

字符串常量池池最后是空的,由 String 类私有地保护。在调用 intern 办法时,如果池中曾经蕴含了由 equals(object)办法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被增加到池中,并返回对该字符串对象的地址。(这是源码里的大略翻译)

如果 不是用双引号申明的 String 对象 ,能够应用 String 提供的 intern 办法: 从字符串常量池中查问以后字符串是否存在,若不存在就会将以后字符串放入常量池中。比方:

String myInfo = new string("I love you").intern();

咱们说 new String()寄存在堆空间,那么就会在堆空间创立 ”I love you “ 同时去常量池判断是否有这个 ”I love you”,若不存在则将以后字符串放入常量池中同时返回地址给 myInfo

这样的话也就说如果在任意字符串上调用 String.intern 办法,那么其返回后果所指向的那个类实例,必须和间接以常量模式呈现的字符串实例完全相同。因而,下列表达式的值必然是 true

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

艰深点讲 interned 对于 String 就是确保字符串在内存里只有一份拷贝,这样能够节约内存空间,放慢字符串操作工作的执行速度。留神: 这个值不存在会创立并存放在字符串外部池(String Intern Pool)

对于 new String() 的阐明

================================

上面咱们看看这个问题:new String(“ab”)会创立几个对象?

依据咱们后面的思路,咱们还间接观看字节码吧,看看到底做了些什么事件

  • new #2 <java/lang/String>:在堆中创立了一个 String 对象
  • ldc #3 <ab>:在字符串常量池中放入“ab”(如果之前字符串常量池中没有“ab”的话)

上面咱们看看这个问题:new String(“a”) + new String(“b”) 会创立几个对象?

依据咱们后面的思路,咱们还间接观看字节码吧,看看到底做了些什么事件

  • new #2 <java/lang/StringBuilder>:拼接字符串会创立一个 StringBuilder 对象
  • new #4 <java/lang/String>:创立 String 对象,对应于 new String(“a”)
  • ldc #5 <a>:在字符串常量池中放入“a”(如果之前字符串常量池中没有“a”的话)
  • new #4 <java/lang/String>:创立 String 对象,对应于 new String(“b”)
  • ldc #8 <b>:在字符串常量池中放入“b”(如果之前字符串常量池中没有“b”的话)
  • invokevirtual #9 <java/lang/StringBuilder.toString>:调用 StringBuilder 的 toString() 办法,会生成一个 String 对象


有了后面两道题做根底,咱们接下来看看一道比拟难的题目看看创立了几个对象

public class StringIntern {public static void main(String[] args) {String s = new String("1");
        s.intern();
        String s2 = "1";
        System.out.println(s == s2);
        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);
    }
}

咱们在 JDK6 的环境下运行看看输入后果是什么

咱们运行起来,发现后果是运行后果:false、false

咱们在 JDK7 的环境下运行看看输入后果是什么

咱们运行起来,发现后果是运行后果:false、true

目前咱们的环境是 JDk7,这时咱们编译看看 main 办法的字节码是怎么样的

而刚刚咱们后面的第二个问题铺垫时就说到过 new String() + new String()的问题解析

那么咱们就剖析一下 s3.intern()是做了什么事件呢?

在后面 new String(“1”) + new String(“1”)的时候会调用 StringBuilder 的 toString() 办法,会生成一个 String 对象为 ”11″,但咱们后面提到过在字符串常量池中并没有生成

所以当咱们执行 s3.intern()的时候

字符串常量池没有 s3 的 ”11″,所以创立一个指向堆空间 new String(“11”)的地址

执行 s4 = “11” 的时候常量池里有 ”11″,所以就会应用 s3.intern()的那个指向地址,所以s3 == s4 为 true

那么咱们一起看看这个解释的思路图吧(JDk7 环境)

那么在 JDK6 的思路图当中就不一样,咱们一起来看看

在 JDK6 当中字符串常量池并没有在堆空间,所以它会在常量池生成一个新的对象 ”11″ 并且有新的地址

所以当在 JDk6 中的 s3 与 s4,它们的援用地址各不同所以s3 == s4 为 false

接下来咱们依据这个个性再扩大一下,看上面的代码块输入后果是什么呢?

public class StringIntern1 {public static void main(String[] args) {String s3 = new String("1") + new String("1");
        String s4 = "11";  
        String s5 = s3.intern();
        System.out.println(s3 == s4);
    }
}

咱们运行起来,发现后果是运行后果:false

那么依据后面的题目剖析,咱们晓得执行 new String(“1”) + new String(“1”)

字符串常量池中并不会存储 ”11″,当执行 s4 = “11” 才会在字符串常量池中存在

而 s5 = s3.intern()其实是在常量池寻找是否有 ”11″,若有则返回指向地址给到 s5

此时 s3 的 ”11″ 是存储在堆空间当中的,但 s4 的 ”11″ 是存储在字符串常量池中,所以为false

小结 intern()

================================

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

应用 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]));
        }
        long end = System.currentTimeMillis();
        System.out.println("破费的工夫为:" + (end - start));

        try {Thread.sleep(1000000);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.gc();}
}
// 运行后果如下:破费工夫:7307

并且咱们通过 Java VisualVm 工具看看这段代码怎么样呢?

咱们再看看应用 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])).intern();}
        long end = System.currentTimeMillis();
        System.out.println("破费的工夫为:" + (end - start));

        try {Thread.sleep(1000000);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.gc();}
}
// 运行后果如下:破费工夫:1311

并且咱们通过 Java VisualVm 工具看看这段代码怎么样呢?

由此可见咱们能够比照两个操作

间接 new String:每个 String 对象都是 new 进去的,所以程序须要保护大量寄存在堆空间中的 String 实例,程序内存占用也会变高

应用 intern() 办法:因为数组中字符串的援用都指向字符串常量池中的字符串,所以程序须要保护的 String 对象更少,内存占用也更低

论断:

对于程序中大量应用存在的字符串时,尤其存在很多曾经反复的字符串时,应用 intern()办法可能节俭很大的内存空间。

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

六、String Table 的垃圾回收


咱们刚刚演示 intern()的执行效率也证实了 String 存在垃圾回收,所以试用 intern()时更省

接下来咱们再通过上面的代码块来领会一下 String 的垃圾回收

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

应用命令:-Xma15m- -Xnx15m XX:+PrintStringTabIeStat1stIcs -XX:+PrintGCDetaiis查看字符串常量池的信息

咱们将循环的操作先正文掉,看看未循环增加时的字符串常量池是怎么样的

这时咱们再运行起来看看,进行循环后的字符串常量池是怎么样的

若咱们将 for 循环的次数减少到十万的话,再运行起来是怎么样的呢?

由十万的输入后果咱们就能够晓得 StringTable 区产生了垃圾回收

  • 在 PSYoungGen 区产生了垃圾回收
  • Number of entries 和 Number of literals 显著没有 100000

七、G1 的 String 去重操作


对于 G1 中对于 String 有去除反复的操作,具体具体可查看官网文档:拜访入口

许多大规模的 Java 利用的瓶颈在于内存,测试表明在这些类型的利用外面,Java 堆中存活的数据汇合差不多 25% 是 String 对象。更进一步,这外面差不多一半 String 对象是反复的,反复的意思是说:str1.equals(str2)= true。

堆上存在反复的 String 对象必然是一种内存的节约。这个我的项目将在 G1 垃圾收集器中实现主动继续对反复的 String 对象进行去重,这样就能避免浪费内存

观看官网文档能够晓得在咱们的利用中个别的堆空间里

  • 堆存活数据汇合外面 String 对象占了 25%
  • 堆存活数据汇合外面反复的 String 对象有 13.5%
  • String 对象的均匀长度是 45

比如说一下代码块,领会一下:

Strnig str1 =new String("hello");    
Strnig str2 =new String("hello");

那么对于这种状况,咱们 G1 是怎么操作的呢?

当垃圾收集器工作的时候会拜访堆上存活的对象。对每一个拜访的对象 都会查看是否它是候选的要去重的 String 对象 如果是:把这个对象的一个 援用插入到队列中期待后续的解决

这时有一个去重的线程在后盾运行解决这个队列。解决队列的时候把元素从队列删除这个元素,而后尝试去援用已有一样的 String 对象

应用一个 Hashtable 来记录所有的被 String 对象应用的不反复的 char 数组。当去重的时候会查这个 Hashtable,来看堆上是否曾经存在一个截然不同的 char 数组

如果存在 String 对象会被调整援用那个数组,开释对原来的数组的援用,最终会被垃圾收集器回收掉。如果查找失败,char 数组会被插入到 Hashtable,这样当前的时候就能够共享这个数组了

提醒:临时理解一下,前面会详解垃圾回收器

对于去重的命令选项如下:

  • UseStringDeduplication(bool):开启 String 去重,默认是不开启的,须要手动开启。
  • PrintStringDeduplicationStatistics(bool):打印具体的去重统计信息
  • stringDeduplicationAgeThreshold(uintx):达到这个年龄的 String 对象被认为是去重的候选对象

参考资料


尚硅谷:JVM 虚拟机(宋红康老师)

正文完
 0