JVM字符串常量池篇(String进阶讲解)

Posted ProChick

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM字符串常量池篇(String进阶讲解)相关的知识,希望对你有一定的参考价值。

1.拼接操作

  • 如果是常量与常量的拼接,原理就是在编译器期间进行的

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

  • 如果是常量与变量的拼接或者变量与变量的拼接,原理就是使用的 StringBuilder

    public void test2(){
        String s1 = "hello";
        String s2 = "world";
    
        String s3 = "helloworld";
        String s4 = "hello" + "world";
    
        String s5 = s1 + "world";
        String s6 = "hello" + 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方法, 则主动将结果放在常量池中,并返回此常量的地址

    public void test2(){
        String s1 = "hello";
        String s2 = "world";
    
        String s3 = "helloworld";
        String s4 = "hello" + "world";
    
        String s5 = s1 + s2;
        String s6 = s5.intern();
    
        System.out.println(s3 == s4);// true
        System.out.println(s3 == s5);// false
        System.out.println(s3 == s6);// true
    }
    

2.拼接原理

从字节码的角度

  • 常量与常量的拼接

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

  • 变量与变量的拼接

    public void test1(){
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        
        /* 
         * 实质的过程
         * ① StringBuilder s = new StringBuilder();
         * ② s.append("a");
         * ③ s.append("b");
         * ④ s4 = s.toString(); 就约等于 new String("ab")
       	 */
        String s4 = s1 + s2;
        
        System.out.println(s3 == s4); // false
    }
    

    注意下面的情况

    public void test1(){
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        
        // 此时我们认为这里不是变量,而是常量引用,所以就不需要使用StringBuilder
        String s4 = s1 + s2;
        
        System.out.println(s3 == s4); // true
    }
    

3.intern方法

String对象存储在字符串常量池中的方式有哪些?

  • 首先我们知道,对于用双引号声明的String对象是直接存储在字符串常量池中的
  • 那么如果不是用双引号声明的String对象,则可以使用String提供的intern方法,intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
  • 通俗点讲,String的intern方法就是确保字符串在内存里只有一份拷贝并且存储在字符串常量池中,这样可以节约内存空间、加快字符串操作任务的执行速度

new String(“ab”)的过程会创建几个对象?

public static void main(String[] args) {
    // 对象1:new String("ab")在堆空间创建的对象
    
    // 对象2:字符串常量池中新创建的对象"ab",当然如果之前有"ab"在字符串常量池存在,则忽略
    String str = new String("ab");
}

new String(“a”) + new String(“b”)的过程会创建几个对象?

public static void main(String[] args) {
    // 对象1:new StringBuilder()在堆空间创建的对象
    
    // 对象2:new String("a")在堆空间创建的对象
    
    // 对象3:字符串常量池中新创建的对象"a"
    
    // 对象4:new String("b")在堆空间创建的对象
    
    // 对象5:字符串常量池中新创建的对象"b"
    
    // 对象6:StringBuilder最后的toString()方法内部, new String("ab")在堆空间创建的对象
    //       此时注意:利用toString()方法间接调用的new String()方法,是不会在常量池中创建"ab"的
    String str = new String("a") + new String("b");
}

关于String.intern的面试题

public static void main(String[] args) {
    // 指向的是堆空间中"a"的内存地址
    String s1 = new String("a");
    // 指向的是字符串常量池中"a"的内存地址, 说明:在调用此方法之前,字符串常量池中已经存在了"a"
    String s2 = s1.intern();
    // 指向的是字符串常量池中"a"的内存地址
    String s3 = "a";
	
    // 判断是否相等
    System.out.println(s1 == s3);  //jdk6:false   jdk7/8:false
    System.out.println(s2 == s3);  //jdk6: true    jdk7/8:true
    
    // 判断变量地址
    System.out.println(System.identityHashCode(s1)); //491044090
    System.out.println(System.identityHashCode(s2)); //644117698
    System.out.println(System.identityHashCode(s3)); //644117698
}
public static void main(String[] args) {
    // 指向的是堆空间中"11"的内存地址
    String s3 = new String("1") + new String("1");
    // 执行此方法时, 发现没有在常量池中发现"11"这个常量
    // 所以在JDK6及之前版本中: 会创建一个新的对象"11", 并把这个对象的内存地址放在常量池中
    // 所以在JDK7及之后版本中: 会创建一个指向堆空间中"11"的内存地址的一个常量, 然后放在常量池中
    s3.intern();
    // 指向的是常量池中"11"的内存地址,由上一步intern方法生成的
    String s4 = "11";
    
    System.out.println(s3 == s4); //jdk6:false  jdk7/8:true
}

public class StringIntern1 {
    public static void main(String[] args) {
        // 指向的是堆空间中"ab"的内存地址
        String s1 = new String("a") + new String("b");
        // 指向的是字符串常量池中"ab", 说明:发现字符串常量池中没有"ab", 则创建"ab"
        String s2 = "ab";
        // 指向的是字符串常量池中"ab", 说明:发现字符串常量池中有"ab", 则返回"ab"
        String s3 = s1.intern();
        
        System.out.println(s1 == s2); //false
        System.out.println(s3 == s2); //true
    }
}

练习1

public static void main(String[] args) {
    // 指向的是堆空间中"ab"的内存地址
    String s = new String("a") + new String("b");
    // 执行此方法时, 发现没有在常量池中发现"ab"这个常量
    // 所以在JDK6及之前版本中: 会创建一个新的对象"ab", 并把这个对象的内存地址放在常量池中
    // 所以在JDK7及之后版本中: 会创建一个指向堆空间中"ab"的内存地址的一个常量, 然后放在常量池中
    String s2 = s.intern();

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

练习2

public static void main(String[] args) {
    // 指向的是堆空间中"ab"的内存地址
    String s1 = new String("ab");
	// 发现字符串常量池中有"ab", 则不作任何操作
    s1.intern();
    // 指向的是字符串常量池中"ab", 说明:发现字符串常量池中有"ab", 则返回"ab"
    String s2 = "ab";
    
    System.out.println(s1 == s2); // false
}

总结intern使用

  • JDK6以及之前
    • 如果字符串常量池中有,则返回已有的串池中的对象的地址
    • 如果字符串常量池中没有,会把此对象复制一份,放入常量池,并返回常量池中的对象地址
  • JDK7以及之后
    • 如果字符串常量池中有,则返回已有的串池中的对象的地址
    • 如果字符串常量池中没有,则会把对象的引用地址复制一份,放入常量池,并返回常量池中的引用地址

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++) {
            // 赋值方式1
            arr[i] = new String(String.valueOf(data[i % data.length]));
            // 赋值方式2
            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

    占用内存240M左右


  • 赋值方式2

    占用内存80M左右


intern的使用场景

  • 对于程序中大量存在存在的字符串,尤其存在很多重复字符串时,使用intern()可以节省内存空间
  • 对于大的网站平台,需要内存中存储大量的字符串信息,比如位置信息、省市级信息

4.垃圾回收程序示例

/**
 * String的垃圾回收:
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 *
 */
public class StringGCTest {
    public static void main(String[] args) {
        // 不会发生垃圾回收行为
        for (int j = 0; j < 100; j++) {
            String.valueOf(j).intern();
        }
        
        // 会发生垃圾回收行为
        for (int j = 0; j < 100000; j++) {
            String.valueOf(j).intern();
        }
    }
}

5.去重处理

  • 研究表明
    • 堆存活数据集合里面String对象占了25%
    • 堆存活数据集合里面重复的String对象有13.5%
    • String对象的平均长度是45
  • 基本概述
    • 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多都是String对象。
    • 但是这里面差不多一半String对象是重复的,重复的意思是说他们的具体值是一样的。
    • 堆上存在重复的String对象必然是一种内存的浪费,而在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就避免了浪费内存
  • 具体实现
    • 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象
    • 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象
    • 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。 当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组
    • 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉
    • 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了
  • 相关参数
    • UseStringDeduplication(bool):开启String去重,默认是不开启的,需要手动开启
    • PrintStringDeduplicationStatistics(bool):打印详细的去重统计信息
    • StringDeduplicationAgeThreshold(uintx):达到这个年龄的String对象被认为是去重的候选对象

以上是关于JVM字符串常量池篇(String进阶讲解)的主要内容,如果未能解决你的问题,请参考以下文章

从JVM的角度解析String

JVM四种常量池全方位细致讲解 这一篇就够了~

JVM——字符串常量池详解

jvm之StringTable(字符串常量池)

Day337&338.StringTable -JVM

八:JVM调优实战及常量池详解