【JVM】StringTable — 字符串常量池

15 篇文章 0 订阅

1. StringTable

不同JDK版本StringTable 位置
StringTable

1.1 String 的基本特性

  • String 字符串,使用一对 “” 引起来表示。
  • String 类声明为 final 的,不可被继承
  • String: 实现了Serializable 接口:表示字符串是支持序列化的, 实现了Comparable 接口:表示String可以比较大小。
  • String在JDK 8及以前内部定义了final char[] value用于存储字符串数据。JDK 9时改为了byte[]。(http://openjdk.java.net/jeps/254

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

public final class String implments java.io.Serializable, Comparable<String>, CharSequence {
    @Stable
    private final byte[] value;
    // ...
}

StringBuffer、StringBuilder 现状:

String-related classes such as AbstractStringBuilder, StringBuilder and StringBuffer will be updated to use the same representation, as will the HotSpot VM’s intrinsic (固有的/内置的) string operations.

  • String:代表不可变的字符序列(不可变性)

    • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
    • 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不饿能使用原有的value进行赋值。
  • 通过字面量的方式(区别于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是可设置的最小值。

1.2 String 的内存分配

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

StringTable为什么要调整?

  1. PermSize 默认比较小
  2. 永久代垃圾回收频率低

官网:

https://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html#jdk7changes

1.3 String 的基本操作

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

1.4 字符串拼接操作

  1. 常量与常量的拼接结果在常量池,原理是编译期优化。
  2. 常量池中不会存在相同内容的常量。
  3. 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder。
  4. 如果拼接的结果调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

测试代码 - 编译期优化:

@Test
public void test1() {
    String s1 = "a" + "b" + "c";	//编译期优化:等同"abc"
    String s2 = "abc";

    /**
         * 最终.java 编译成.class, 再执行.class
         * String s1 = "abc"
         * String s2 = "abc";
         */
    System.out.println(s1 == s2);       //true
    System.out.println(s1.equals(s2));  //true
}

测试代码 - 拼接前后出现变量:

@Test
public void test2() {
    String s1 = "javaEE";
    String s2 = "hadoop";

    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";   //编译期优化:等同"javaEEhadoop"

    // 如果拼接符号前后出现变量,则相当于在堆空间中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);   //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 并返回此对象的地址。
    String s8 = s6.intern();
    System.out.println(s3 == s8);   //true
}

测试代码 - 拼接前后出现变量:

@Test
public void test3() {
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    /**
         * 如下的 s1 + s2 的执行细节(连接符号两边出现 变量,s是临时变量):
         * 1、StringBuilder s = new StringBuilder();
         * 2、s.append(s1);
         * 3、s.append(s2);
         * 4、s.toString();   --->   (堆)约等于 new String("ab")
         *
         * 补充:在JDK 5.0 之后使用的是StringBuilder,在JDK 5.0 之前使用的是StringBuffer
         */

    String s4 = s1 + s2;
    System.out.println(s3 == s4);   //false
}

结果

测试代码 - 拼接前后为常量:

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

字符串拼接操作不一定使用的是StringBuilder:

  • 如果拼接符号左右两边都是字符串常量("")或常量引用(final),则仍然使用编译期优化

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

测试代码 - 拼接 & append 效率比对:

public void method1() {
    String str = "";
    for (int i = 0; i < 100000; i++) {
        str = str + "a";	//每次循环都会创建一个StringBuidler、String
    }
}

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

@Test
public void test() {
    long startTime = System.currentTimeMillis();

    //method1();
    //method2();

    long endTime = System.currentTimeMillis();

    System.out.println("消耗时间:" + (endTime - startTime));
}

调用 method1:

调用method1

调用 method2:

调用method2

通过StringBuilder的append()方法添加字符串的效率 远高于 使用String的字符串拼接方式。

对比:

  • 创建对象问题:
    • StringBuilder的append()方法:自始至终之仅创建一个对象
    • String的字符串拼接方式:创建过多个 StringBuilder 和 String 对象
  • String的字符串拼接方式:内存中由于创建过多个 StringBuilder 和 String 对象,内存占用过大,GC需花费额外时间

优化:

  • 在实际开发中,如果基本确定前前后后添加的字符串长度不高于某个限定值highLevel,通过构造器为StringBuilder指定capacity容量highLevel(避免数组频繁扩容占用内存, StringBuilder s = new StringBuillder(highLevel);)

1.5 intern() 的使用

1.5.1 概述

intern():判断字符串常量池中是否存在 javaEEhadoop 的值:

  • 存在:则返回常量池中这个字符串的地址。
  • 不存在:则在常量池中加载一份 javaEEhadoop 并返回此对象的地址。

s.intern() == t.intern() 等价于 s.equals(t)


如果表示用双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

// 比如:
String myInfo = new String("I love you").intern();

即,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的类实例,必须和直接以常量形式出现的字符串实例完全相同。

因此,下列表达式的结果必定是true:

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

通俗点讲,Interned String 就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被放在字符串内部池(String Intern Pool)。

/**
 * 如何保证变量s指向的是字符串常量池中的数据呢?
 * 两种方式:
 * 1、字面量声明方式:String s = "ABC";
 * 2、intern()方法:String s = new String("ABC").intern();
 * 	  		      String s = new StringBuidler("ABC").toString().intern();
 */

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

  • String对象有两个:
    • 一个对象是通过new 关键字在堆空间创建
    • 一个对象是常量池中的"ab"(字节码指令ldc)

展示

扩展:new String(“a”) + new String(“b”)会创建几个对象?

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

展示

  • 深入剖析:StringBuilder 的 toString():
    • 对象6:new String(“ab”)
  • 调用 toString(),在字符串常量池中,没有生成"ab"

1.5.2 举例

JDK 6 – vs – JDK 7/8

public class StringIntern1 {
    public static void main(String[] args) {
        
        /// 1、第一种情况
        String s = new String("1");
        s.intern();			// 调用此方法之前,字符串常量池中已经存在了"1"
        String s2 = "1";
        System.out.println(s == s2);
        /** 
         * JDK 6 + 7/8 :  
         *       false:
         *            s : new关键字在堆空间创建的地址
         *            s1: 字符串常量池中对象的地址
		 */
        
        /// 2、第二种情况
        String s3 = new String("1") + new String("1");		//s3变量记录的地址为:new String("11")
        // 执行完上一行代码后,字符串常量池不存在"11"
        s3.intern();			// 在字符串常量池中生成"11":JDK6:创建了一个新的对象"11",也就有新的地址。JDK7:此时常量池中并没有创建"11",而是指向(记录)堆空间之前new的"11"的地址
        String s4 = "11";		// s4变量记录的地址:上一行代码执行时,在常量池中生成的"11"的地址
        System.out.println(s3 == s4);
    }
}

JDK 6:false + false

JDK 7/8:false + true

JDK 8

public class StringIntern2 {
    public static void main(String[] args) {
        /// 3、第三种情况
        String s3 = new String("1") + new String("1");
        // 执行完上一行代码后,字符串常量池不存在"11"
        String s4 = "11";	// 在字符串常量池中生成对象"11"
        String s5 = s3.intern();	//常量池中已经生成对象"11",这一步仅可使s5持有常量池中地址
        System.out.println(s3 == s4);	//false
        System.out.println(s5 == s4);	//true
    }
}

总结String的intern()的使用:

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

1.5.3 intern() : 空间效率测试

/**
 * 使用intern测试效率:空间角度
 */
public class StringInternTest {
    static final int MAX_COUNT = 1000 * 1000;
    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++) {
           /* 测试焦点:是否调用 intern() 方法 */
           //  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();
    }
}

未使用intern():

结果

展示

使用intern():

结果

展示

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

大型网站平台,需要内存中存储大量字符串(社交网站)。这是候如果字符串都调用 intern() 方法,就会明显降低内存大小。

1.6 StringTable的垃圾回收

/**
 * String 的垃圾回收:
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 *
 * 参数 -XX:+PrintStringTableStatistics : 打印字符串常量池统计信息
 */
public class StringGCTest {
    public static void main(String[] args) {
        // 依次调大参数,0 -> 100 -> 100000,观察是否发生GC
        for (int i = 0; i < 100; i++) {
            String.valueOf(i).intern();
        }
    }
}

0 (取消for循环):

0

100:

100

100000:

发生GC:

100000

参数不足100000:

100000

1.7 G1中的String去重操作

官网:

http://openjdk.java.net/jeps/192

去重:针对char型数组

String str1 = new String("hello");
String str2 = new String("hello");
  • 背景:对许多Java应用(有大有小)做的测试得出以下结果:

    • 堆存活数据集合里面String对象占了25%
    • 堆存活数据集合里面重复的String对象有13.5%
    • String对象平均长度是45
  • 许多大规模的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对象被认为是去重的候选对象

对每一个访问的对象都会检查是否是候选的要去重的String对象。

  • 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。

  • 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。去重时,查询hashtable,看堆上是否已经存在一个一模一样的char数组。

  • 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。

  • 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。

  • 命令行选项

    • UseStringDeduplication (bool):开启String去重,默认不开启,需手动开启。
    • PrintStringDeduplicationStatistics (bool):打印详细的去重统计信息
    • StringDeduplicationAgeThreshold (uintx):达到这个年龄的String对象被认为是去重的候选对象
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值