JVM基本结构-String Table

笔记参考尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ

1.String的基本特性

  • String:字符串,使用一对””引起来表示
  • String声明为final的,不可被继承
  • String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口: 表示string可以比较大小
  • String在jdk8及以前内部定义了final char[] value用于存储字符串数据。jdk9时改为final byte[] value。

 

String在jdk9中存储结构变更

官网:https://openjdk.org/jeps/254

官网对他的描述:

机翻的,将就一下。

 

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

那StringBuffer 和 StringBuilder 是否仍无动于衷呢?

 

翻译

  • String:代表不可变的字符序列。简称:不可变性。
    (1)当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
    (2)当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值不能使用原有的value进行赋值。
    (3)当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值, 不能使用原有的value进行赋值。

 

代码演示

字符串常量池不允许存放相同的字符串

package com.yutian.java;

import org.junit.Test;

/**
* String的基本使用:体现String的不可变性
*/
public class StringTest1 {
    @Test
    public void test1() {
        String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中
        String s2 = "abc";
        //s1 = "hello";

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

        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(s1不变)
        System.out.println(s2);//mbc(新对象)
    }
}

 

  • 通过字面量的方式(区别于new) 给一个字符串赋值,此时的字符串值声明在字符串常量池中。

package com.yutian.java;


public class StringExer {
    String str = new String("琪亚娜");
    char[] ch = {'幽', '兰', '呆', '尔'};

    public void change(String str, char ch[]) {
        str = "卡斯兰娜";
        ch[2] = '黛';
    }

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

}

输出结果如下:

 

java的基本数据类型是传值调用,引用类型是传引用调用:

  1. 基本数据类型传值,对形参的修改不会影响实参
  2. 引用类型传引用,形参和实参指向同一个内存地址(同一个对象),对参数的修改会影响到实际的对象

 

String是引用类型,引用类型是引用传递,那么那么结果应该是“卡斯兰娜”,但是确是”琪亚娜”。

java在方法传递参数时,是将变量复制一份,然后传入方法体执行。

所以对于基本数据类型传值,形参的修改不会影响实参。

而上图并不是将对象传入,而是将String作为参数,类似值传递,会将str拷贝,change方法中实际是对拷贝的值进行操作,使其指向“卡斯兰娜”,而不改变原对象的str;而char数组是将数组的地址作为参数,修改char数组实际就是修改数组的内容,为引用传递。

package com.yutian.java;

public class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Person p1 = new Person("琪亚娜");
        change(p1);
        System.out.println(p1.name);
    }

    public static void change(Person p2) {
        p2.name = "幽兰呆鹅";
    }

}

这时就输出”幽兰呆鹅”了,因为这是将对象的地址作为参数,对参数的修改会影响到实际的对象。

String,Integer,Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会影响修改实参对象

 

  • 字符串常量池中是不会存储相同内容的字符串的。
  • String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。
  • 使用-XX:StringTableSize可设置StringTable的长度。
  • 在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求。
  • 在jdk8中,StringTable的长度默认值是60013,1009是可设置的最小值。
  • 否则会报下图的错。

可以自己测试一下:

jps
jinfo -flag StringTableSize 进程号

 

2.String的内存分配

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

 

3.String的基本操作

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

package com.yutian.java1;


public class StringTest4 {
    public static void main(String[] args) {
        System.out.println();//2293
        System.out.println("1");//2294
        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");//2303


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

先打开Memory

初始2233个

加载完前面共2243个

同样的字符串不会再被存储,依然是2243个

 

package com.yutian.java1;

class Memory {
    public static void main(String[] args) {
        int i = 1;
        Object obj = new Object();
        Memory mem = new Memory();
        mem.foo(obj);
    }

    private void foo(Object param) {
        String str = param.toString();
        System.out.println(str);
    }
}

对应情况如下:

 

4.字符串拼接操作

对于new String(“XXXX”)或new StringBuilder(“XXXX”)或new StringBuffer(“XXXX”),如果字符串常量池没有”XXXX”,就会在字符串常量池中创建”XXXX”。务必牢记这点。

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

代码如下:

@Test
public void test1(){
    String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
    String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
    System.out.println(s1 == s2); //true
    System.out.println(s1.equals(s2)); //true
}

编译后,打开out目录下的.class文件,这就是编译期优化

 

打开jclasslib一样,都一样指向#2

 

2.常量池中不会存在相同内容的常量。
3.只要其中有一个是变量,结果就在堆中(非常量池中)。变量拼接的原理是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;

    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
}

输出结果

先看一看编译期优化,打开out目录下

 

(1)如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop

(2)intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址;如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回次对象的地址。

看以下情况:

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

编译后

因为this保存在索引为0的位置,所以s1从索引为1开始。

步骤如下:

如下的s1 + s2 的执行细节:(变量s是我临时定义的)

  • ① StringBuilder s = new StringBuilder();
  • ② s.append(“a”)
  • ③ s.append(“b”)
  • ④ s.toString() –> 约等于 new String(“ab”),注意存在差别,在下面会讲。

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

看以下情况:

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

编译优化后

fclasslib中

输出结果

 

1. 字符串拼接操作不一定使用的是StringBuilder! 如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式。加了final修饰类相当于常量了。

2. 针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。

体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!

代码如下:

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

}

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

测试method1时间

@Test
public void test6(){

    long start = System.currentTimeMillis();

    method1(100000);

    long end = System.currentTimeMillis();

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

 

测试method2时间

@Test
public void test6(){

    long start = System.currentTimeMillis();

    method2(100000);

    long end = System.currentTimeMillis();

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

method2时间远远小于method1

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

 

StringBuilder底层默认初始大小为char[16],大于会扩容。

 

改进的空间:在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,

建议使用构造器实例化:StringBuilder s = new StringBuilder(highLevel);//new char[highLevel],这样可以避免StringBuilder放不下而频繁扩容。

 

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

 

补充:

String 中 == 比较引用地址是否相同,equals() 比较字符串的内容是否相同:

@Test
public void test_1(){
    String s1 = "Hello";

    String s2 = new String("Hello");

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

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

}

S1指向的是String Pool里的对象,S2指向的是堆里的对象,两者并非同一个对象,但字符串相同

所以

 

@Test
public void test_1(){
    String s1 = new String("Hello");

    String s2 = new String("Hello");

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

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

不是同一个对象,但是字符串内容相同。输出如下:

String s2 = new String(“Hello”),这个虽然结果在堆中(非常量池中),但是依然是此对象指向字符串常量池中的“Hello”。

String s2 = new String(“Hello”),是在堆中创建一个”Hello”,然后看看常量池中有没有”Hello”,没有也给创建一个,但是返回的是堆中的对象地址。

ldc取出字符串常量池相应的值

 

当然,S1和S2都new了一个String对象,肯定S1和S2的引用地址不同。

 

5.intern()的使用

查看String的源码,存在一个native的intern()方法

大致意思是

 

intern()的使用

  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法,intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如: String myInfo = new String(“I love 琪亚娜”).intern();
  • 也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式(“a”+”b”+”c”).intern() ==”abc”必定是true。
  • 通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(string Intern Pool) 。
@Test
public void test_1(){
    String s1 = new String("琪亚娜");
    String s2 = "琪亚娜";
    System.out.println(s1 == s2);
}

依我们所学,S1是一个对象再指向字符串常量池的”琪亚娜”,而S2直接指向字符串常量池的”琪亚娜”,所以输出False

@Test
public void test_1(){
    String s1 = new String("琪亚娜").intern();
    String s2 = "琪亚娜";
    System.out.println(s1 == s2);
}

而用上.intern()方法,intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中,且直接指向,所以输出true

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

有两种方式:
  • 方式一: 字面量定义的方式
    String s = “shkstart”;
  • 方式二: 调用intern()
    String s = new String(“shkstart”).intern();
    String s = new StringBuilder(“shkstart”).toString().intern();


面试题:


题目: new string(“ab”) 会创建几个对象?

一个或两个对象

证明:
看代码

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

一个String对象,和字符串“ab”(如果字符串常量池没有”ab”)。

  • 一个对象是:new关键字在堆空间创建的
  • 另一个对象是:字符串常量池中的对象”ab”。 字节码指令:ldc

 

 

折展: 那new string(“a”) + new strinq(“b”)呢?

 

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

 

三个String对象,和字符串”a”和”b”(如果字符串常量池没有“a”和”b”),但不止5个

  • 对象1:new StringBuilder()
  • 对象2: new String(“a”)
  • 对象3: 常量池中的”a”
  • 对象4: new String(“b”)
  • 对象5: 常量池中的”b”

前面说过,String字符串拼接,StringBuilder为了将自身转换成String,会使用自身的.toString方法

 

StringBuilder的.toString方法,会new一个String对象,所以加起来应该六个对象。

 

深入剖析: StringBuilder的toString():
对象6 :new String(“ab”)

强调一下,toString()的调用,在字符串常量池中,没有生成”ab”。

将光标放在 StringBuilder的toString()上,打开jclasslib

 

在方法中找到toString

只new了一个String

aload_0表示从局部变量表的位置0装载一个对象引用到操作数栈的栈顶。

同时,没有ldc指令,即不会从字符串常量池中取出。

 

此时,也就是说这种方式,字符串常量池没有”ab”,而是在堆中。

 

intern()的使用:jdk6 vs jdk7/8:

以下这题,在jdk的这几个版本结果不一样

package com.yutian.java2;


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


        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 jdk7/8:true
    }


}

jdk6输出

 

jdk7或8输出

第一个输出毫无疑问,因为不同对象。

但是第二个,为什么在不同版本会不同呢?

首先

String s3 = new String("1") + new String("1");

此时s3变量记录的地址为:new String(“11”)

执行完上一行代码以后,字符串常量池中,是否存在”11″呢?

答案:不存在!!上面说过的。

 

对了, 执行了s3.intern();后没有赋值回去给s3,则s3是完全没有变化的。

但是,.intern()被使用了,intern方法就会从字符串常量池中查询当前字符串是否存在,若不存在就会在字符串常量池中创建一个,至于创建一个什么,在jdk6和jdk7以后截然不同。

 

在字符串常量池中生成什么。如何理解:

使用.intern()

  • jdk6:创建了一个新的对象”11″,也就有新的地址。intern()方法在常量池中的进行如下操作,如果有”11″就返回已有”11″的地址值,如果没有就在常量池中创建一个”11″并返回。
  • jdk7:此时常量池中并没有创建”11″,而是创建一个指向堆空间中new String(“11”)的地址 ,jdk7开始intern()方法逻辑变了。如果常量池中没有”11″,不是立即创建,而是看看堆中有没有,如果有就直接用堆中的数据,在常量池中创建一个指向堆空间中数据的地址。也就是说此时String s4 = “11”,就算常量池中并没有”11″,也不会在常量池中创建”11″,而是使用那个指向堆空间中数据的地址,实质上的s4是先指向常量池中刚创建的那个指向堆空间中数据的地址的地址,再指向堆空间中数据的地址。所以两个引用指向的都是堆中的”11″,返回true。

 

 

看代码,交换上面的顺序
package com.yutian.java2;


public class StringIntern1 {
    public static void main(String[] args) {
        //StringIntern.java中练习的拓展:
        String s3 = new String("1") + new String("1");//new String("11")
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
        String s4 = "11";//在字符串常量池中生成对象"11"
        String s5 = s3.intern();
        System.out.println(s3 == s4);//false
        System.out.println(s5 == s4);//true
    }
}

运行

String s4 = “11”执行后,会在常量池中创建”11″,此时再执行String s5 = s3.intern(),s5指向的是s4所指的常量池中的”11″。

 

同理:

package com.yutian.java2;

public class StrIngIntern3 {
    public static void main(String[] args) {
        String s = new StringBuilder("琪亚娜").append(" 卡斯兰娜").toString();
        System.out.println(s == s.intern());

        String s1 = new StringBuilder("琪亚娜").toString();
        System.out.println(s1 == s1.intern());

    }
}

输出结果

 

第一个true,

因为StringBuilder(“琪亚娜”).append(” 卡斯兰娜”)如上图所说,没有在字符串常量池创建”琪亚娜 卡斯兰娜”,调用intern()方法后,在jdk7以后,会看看堆中有没有,如果有就直接用堆中的数据,在常量池中创建一个指向堆空间中数据的地址。所以两个引用指向的都是堆中的”琪亚娜 卡斯兰娜”,所以为true.

 

第二个false,

很明显,s1是一个对象,而s1.intern()直接指向了”琪亚娜”。返回false。

在看一个案例:

package com.yutian.java2;

public class StrIngIntern3 {
    public static void main(String[] args) {
        System.out.println(("a"+"b"+"c").intern() =="abc");
        System.out.println(new StringBuilder("琪亚娜").toString().intern() == "琪亚娜");
        System.out.println(new StringBuilder("琪亚娜").append("和芽衣").toString().intern() == "琪亚娜和芽衣");
        System.out.println(new String("I love 琪亚娜").intern() == "I love 琪亚娜");
    }
}
输出结果

我们看第一个

System.out.println(("a"+"b"+"c").intern() =="abc");

编译优化后,相当于

都指字符串常量池的”abc”。

 

我们看第二个

System.out.println(new StringBuilder("琪亚娜").toString().intern() == "琪亚娜");

new StringBuilder(“琪亚娜”).toString(),此时”琪亚娜”已经在常量池中,.intern()方法后,s1就指向”琪亚娜”的地址。

 

我们看第三个

System.out.println(new StringBuilder("琪亚娜").append("和芽衣").toString().intern() == "琪亚娜和芽衣");

因为StringBuilder(“琪亚娜”).append(“和芽衣”)如上图所说,没有在字符串常量池创建”琪亚娜和芽衣”,调用intern()方法后,在jdk7以后,会看看堆中有没有,如果有就直接用堆中的数据,在常量池中创建一个指向堆空间中数据的地址。而”琪亚娜和芽衣”,就算常量池中并没有”琪亚娜和芽衣”,也不会在常量池中创建”琪亚娜和芽衣”,而是使用指向常量池中刚创建的那个指向堆空间中数据的地址的地址,再指向堆空间中数据的地址。所以两个引用指向的都是堆中的”琪亚娜和芽衣”,返回true。

 

我们看第四个

new String(“I Love 琪亚娜”),此时”I Love 琪亚娜”已经在常量池中,.intern()方法后,就指向常量池中”I Love 琪亚娜”的地址。

上述代码编译优化后,相当于

 

如果不使用intern()方法

package com.dreams.test;
 
public class StrIngIntern3 {
    public static void main(String[] args) {
        System.out.println(("a"+"b"+"c") =="abc");
        System.out.println(new StringBuilder("琪亚娜").toString() == "琪亚娜");
        System.out.println(new StringBuilder("琪亚娜").append("和芽衣").toString() == "琪亚娜和芽衣");
        System.out.println(new String("I love 琪亚娜") == "I love 琪亚娜");
    }
}

编译优化后,相当于

输出

 

避免混淆,看下面代码:

package com.dreams.test;
 
public class StrIngIntern3 {
    public static void main(String[] args) {
        System.out.println(("a"+"b"+"c") =="abc");
        System.out.println(new StringBuilder("琪亚娜").toString() == "琪亚娜");
        System.out.println(new StringBuilder("琪亚娜").append("和芽衣").toString() == "琪亚娜和芽衣");
        System.out.println(new String("I love 琪亚娜") == "I love 琪亚娜");

        System.out.println("琪亚娜" + "和芽衣" == "琪亚娜和芽衣");
    }
}

 

总结String的intern()的使用:

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

 

练习:

看看代码

package com.yutian.java2;


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

        String s2 = s.intern();
       
        System.out.println(s2 == "ab");//jdk6:true jdk8:true
        System.out.println(s == "ab");//jdk6:false jdk8:true
    }
}

 

String s = new String(“a”) + new String(“b”)会在堆存一个”ab”,在上一行代码执行完以后,字符串常量池中并没有”ab”

此时:

  • jdk1.6中

String s2 = s.intern()在jdk6中:在串池中创建一个字符串”ab”

输出true 和 false

 

  • jdk8中

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

输出true 和 true

 

对比:

String s1 = new String(“ab”)执行完以后,会在字符串常量池中会生成”ab”
String s1 = new String(“a”) + new String(“b”)执行完以后,不会在字符串常量池中会生成”ab”,在堆。

 

.intern()的空间效率测试

代码:

package com.yutian.java2;

/**
* 使用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();
    }
}

上述通过这两种方法测试

arr[i] = new String(String.valueOf(data[i % data.length]));
arr[i] = new String(String.valueOf(data[i % data.length])).intern();

分别测试一下,使用intern()测试执行效率,主要测试空间使用上的

使用 jvisualvm,点击抽样器,再点击内存。

arr[i] = new String(String.valueOf(data[i % data.length]));

 

arr[i] = new String(String.valueOf(data[i % data.length])).intern();

 

明显使用了intern()方法更少。

虽然,字符串常量池里不会存在相同的字符串,但是,每一次new String 都会在堆中创建对象,使用了intern()方法,就不会再次在堆中创建一个对象,而是都指向字符串常量池。而前面new的对象,不用就会被销毁。

 

题外话:

使用JProfiler一样可以查看

点击Live memory

 

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

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

 

6.StringTable的垃圾回收

看一个简单代码

package com.yutian.java3;


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

String.valueOf(j)内部实际也是调用new String

toString()方法内部

加入参数:

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

参数解释:

  • -Xms15m -Xmx15m设置堆空间初始和最大
  • -XX:+PrintGCDetails打印GC信息和堆空间占用大小使用。
  • -XX:+PrintStringTableStatistics打印字符串常量池信息

 

先注释掉重点代码

在VM options加入参数

没有GC信息,但有堆信息

字符串常量池信息

 

打开注释

100次循环,涨了100,再加上几个额外的字符串。

变成10000次循环

此时还没有垃圾回收

再加到100000

此时就远远少于循环次数。

因为发生了GC,新生代的YoungGC

 

7.G1中的String去重操作

官网介绍

https://openjdk.org/jeps/192

 

这里并不是字符串常量池的去重,因为字符串常量池本来就不存在。

 

比如:

String str1 = new String(“Dreams”)

String str2 = new String(“Dreams”)

如上图,明明指向字符串常量池中同一个”Dreams”的,却在堆中创建了两个对象。也就是string1.equals(string2)=true针对这种情况的去重。

其实也就是对String中的Char数组的去重

 

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

  • 堆存活数据集合里面String对象占了25%。
  • 堆存活数据集合里面重复的String对象有13.5%。
  • String对象的平均长度是45。

(2)许多大规模的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对象被认为是去重的候选对象。

 

 

参考资料:

尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ

周志明:深入理解java虚拟机

张秀宏:自己动手写Java虚拟机 (Java核心技术系列)

Java虚拟机规范:Chapter 4. The class File Format (oracle.com)

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇