JVM StringTable

15 篇文章 0 订阅
本文详细介绍了Java中的String类的基本特性,包括其不可变性、内存分配方式,特别是字符串常量池的运作机制。提到了intern()方法的作用,以及不同版本JDK中字符串常量池的位置变化。还讨论了字符串拼接的效率问题和垃圾回收对String对象的影响。
摘要由CSDN通过智能技术生成

String的基本特性

  • String: 字符串,使用一对 " "引起来表示,

String s1 = "doubily"; // 字面量的含义

String s2 = new String("doubily");

  • String 的声明为 final的,不可被继承
  • String 实现了 Serializable 接口: 表示字符串是支持序列化的,实现了 Comparable接口: 表示 String 可以比较大小
  • String 1.9 再也不用 char[] 来存储了,改为了 byte[] 加上编码标记,节约了一些空间
  • String 代表不可变的字符序列,当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的 value 进行赋值,当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的 value 赋值。 当调用 String 的 replace() 方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值
public class StringTest1 {

    public static void main(String[] args) {
        test1();
        test2();
        test3();
    }
    public static void test1() {
        String s1 = "abc";  // 字面量定义的方式,"abc" 存储在字符串常量池
        String s2 = "abc";

        //s1 = "hello";
        System.out.println(s1 == s2);
        System.out.println(s1);
        System.out.println(s2);
    }


    public static void test2() {
        String s1 = "abc";
        String s2 = "abc";
        s2 += "def";
        System.out.println(s1);
        System.out.println(s2);
    }

    public static void test3() {
        String s1 = "abc";
        String s2 = s1.replace('a','m');
        System.out.println(s1);
        System.out.println(s2);
    }
}
  • 通过字面量的方式(区别于 New) 给一个字符串赋值,此时的字符串值声明在字符常量池中

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

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

String的内存分配

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

1. 直接使用双引号声明出来的 String 对象会直接存储在常量池种

        String info = "doubily";

2. 如果不是使用双引号声明的 String 对象,可以使用 String 提供的 Intern() 方法

  • java 6 以及之前,字符串常量池存放在永久代
  • java 7及以后,  将字符串常量池调整到 java 堆内
/**
 * 设置永久代大小
 * jdk6: --XX:PermSize=6m --XX:MaxPermSize=6m -Xms6m -Xmx6m
 * jdk8: MetaspaceSize=6m --XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 */
public class StringTest3 {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();

        short i = 0;
        while (true){
            set.add(String.valueOf(i++).intern());
        }
    }
}

StringTable 为什么要调整:

  • permSize 默认比较小
  • 永久代垃圾回收频率低

字符串拼接操作

  • 常量与常量的拼接结果在常量池,原理是编译期优化
  • 常量池种不会存在相同内容的常量
  • 只要其中有一个是变量,结果就在堆中,变量拼接的原理是StringBuilder
  • 如果拼接的结果调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
public class StringTest5 {
    public static void main(String[] args) {
        test1();
    }
    public static void test1(){
        String s1 = "a" + "b" + "c";

        String s2 = "abc"; // 一定是放在字符串常量池中,将此地址

        System.out.println(s1 == s2); // true 编译期优化
        System.out.println(s1.equals(s2));
    }

    public static void test2(){
        String s1 = "javaEE";
        String s2 = "hadoop";
        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";
        // 如果拼接符号的前后出现了变量,则相当于在堆空间中 new String()
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2;

        System.out.println(s3 == s4); // true 编译期优化
        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);

        // intern 判断字符串常量池中是否存在值,如果存在,则返回常量池中的地址,如果不存在,则在常量池中加载一份,并返回对象地址
        String s8 = s6.intern();
        System.out.println(s3 == s8);
    }

    public static void test3(){
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        /**
         * StringBuilder s = new StringBuilder();
         * s.append("a");
         * s.append("b");
         * s.toString(); 。类似与 new String()
         * 在jdk5.0 之后使用 StringBuilder, 在jdk5.0 之后使用 StringBuffer
         */
        String s4 = s1 + s2;
        System.out.println(s3 == s4);
    }
    public static void test4(){
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        // 如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非 StringBuilder 方法
        // 针对 final修饰类、方法、基本数据类型、引用数据类型的量的结构,能使用 final 建议使用上
        String s4 = s1 + s2;
        System.out.println(s3 == s4);
    }

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

    /**
     * 通过 StringBuilder 的 append() 的方式添加字符串的效率要远高于使用 String 的字符串拼接方式
     * StringBuilder 的 append() 的方法自始至终只创建一个 StringBuilder 对象
     * String 字符串拼接方式:创建多个 StringBuilder 和 String 对象
     * 使用 String 的字符串拼接方式: 内存中由于创建了较多的 StringBuilder 和 String ,占用大量内存
     * @param highLevel
     */
    public void method2(int highLevel){
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < highLevel; i++){
            sb.append("a"); // 每次循环都会创建一个StringBuilder, string
        }
    }
}

intern() 的使用

  • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法: intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如: String myInfo = new String("doubily").intern();
  • 在任意字符串上调用 String.intern 方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串完全相同。因此 ("a" + "b" +"c").intern() == "abc" 为 true;
  • Interned String 就是确保字符串在内部里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
  • jdk1.6 中,如果串池中有,则并不会放入。返回已有的串池中的对象地址,如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
  • jdk1.7起,如果串池中有,则并不会放入。返回已有的串池中的对象地址,如果没有,则会把对象引用地址复制一份,放入串中,并返回串池中的引用地址
/**
 * String s = "doubily"; // 字面量定义的方式
 * String s = new String("doubily").intern();
 */
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  s 堆空间的引用地址 s2 常量池地址

        String s3 = new String("1") + new String("1"); // s3 变量记录的地址为: new String("11") 字符串常量池中不存在 11
        s3.intern(); // 在字符串常量池中生成 "11", jdk6: 创建了一个新的对象 "11",也就有了新的地址 jdk7: 常量池记录了创建 "11" 的地址
        String s4 = "11"; // 使用的是上一行代码执行时,在常量池中生成的 "11" 的地址
        System.out.println(s3 == s4);// jdk6 false jdk7/8: true

        String s5 = new String("1") + new String("1"); // String("11")
        String s6 = "11";  // 常量池中生成对象 "11"
        s5.intern();
        String s7 = s5.intern();
        System.out.println(s5 == s6);// jdk6 false jdk7/8: false
        System.out.println(s7 == s6); // true

    }
}

new String("ab") 会创建几个对象:

public class StringNewTest {
    public static void main(String[] args) {

        /**
         * 2 个对象,看字节码
         *  0 new #2 <java/lang/String> 一对象是通过 new 在堆空间创建的
         *  3 dup
         *  4 ldc #3 <ab> 字符串常量池中的对象 ldc
         *  6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
         *  9 astore_1
         * 10 return
         */
        // String str = new String("ab");
        /**
         *  0 new #2 <java/lang/StringBuilder> new StringBuilder()
         *  3 dup
         *  4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
         *  7 new #4 <java/lang/String> new String("a")
         * 10 dup
         * 11 ldc #5 <a> 常量池中的 "a"
         * 13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
         * 16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
         * 19 new #4 <java/lang/String> new String("b")
         * 22 dup
         * 23 ldc #8 <b> 常量池中的 "b"
         * 25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
         * 28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
         * 31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> // new String("ab") 字符串常量池中不存在 ab
         * 34 astore_1
         * 35 return
         */
        String str = new String("a") + new String("b");
    }
}

熟悉1:

Intern() 空间测试:

/**
 * 对于字符串尤其存在很多重复字符串时,使用 intern() 可以节省很多空间
 */
public class StringExer2 {
    static final int MAX_COUNT = 1000 * 10000;

    static final String[] arr = new String[MAX_COUNT];

    public static void main(String[] args) throws InterruptedException {
        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));
        Thread.sleep(10000000);
    }
}

StringTable 的垃圾回收

/**
 * 垃圾回收 -Xms15m -Xmx15m -XX:+PrintGCDetails -XX:+PrintStringTableStatistics
 */
public class StringGCTest {
    public static void main(String[] args) {
        for (int j = 0; j < 1000000; j++) {
            String.valueOf(j).intern();
        }
    }
}

G1 中的 String 去重操作

背景:

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

实现:

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

命令行:

  • UseStringDeduplication(bool): 开启 String 去重,默认是不开启的,需要手动开启
  • PrintStringDeduplicationStatistics(bool): 打印详细的去重统计信息
  • StringDeduplicationAgeThreshold(uintx): 达到这个年龄的 String 对象被认为是去重的候选对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值