StringTable---20200525

14 篇文章 0 订阅

《尚硅谷JVM系列》P118-P133

1. String的基本特性

  • 字符串,用""来表示
    • String s1 = “abc”;
    • String s1 = new String(“abc”);
  • 声明为final,不可被继承;实现了Serializable接口:表示字符串是支持序列和的;实现了Comparable接口:表示可比较大小。
    public final class String implements java.io.Serializable, Comparable<String>, CharSequence {}
  • String存储字符串数据的结构在jdk9发生了更改:大部分String对象只包含Latin-1字符,用一个字节就能存下,因此用char(两个字节)有不少空间浪费。
    更改之后,用byte[]加上编码标记,节约了一些空间:ISO-8859-1/Latin-1用一个字节存,UTF-16用两个字节存。
    • (jdk8) private final char[] value;
    • (jdk9) private final byte[] value;

一个String相关面试题:

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 exer = new StringExer();
        exer.change(exer.str,exer.ch);
        System.out.println(exer.str); //good
        System.out.println(exer.ch); //best
    }
}
  • 字符串常量池中是不会存储相同内容的字符串的
    • String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成hash冲突严重,从而链表会很长,调用String.intern()时性能会大幅下降。
    • 使用-XX:StringTableSize可设置StringTable的长度。
    • jdk6中StringTable长度是固定的,就是1009,StringTableSize设置没有要求。
    • jdk7中,StringTable默认长度是60013,StringTableSize设置没有要求。
    • jdk8中,StringTable默认长度是60013,1009是可设置的最小值

2. String的内存分配

  • 在java语言中有8中基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行时速度更快、更节省内存,都提供了一个常量池的概念。
  • 常量池就类似一个Java系统级别提供给的缓存。8种基本数据类型的常量池都是系统协调的,String类型比较特殊,它的主要使用方法有两种:
    • 直接用双引号生成的String对象会直接存储在常量池中,如String s1 = "abc"
    • 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
  • java6以前,字符串常量池放在永久代
  • java7,把字符串常量池放到堆中。
    • 所有的字符串都保存在堆中,和其它普通对象一样,这样可以让你在进行调优应用时仅需调整堆大小就行。
    • 字符串常量池概念原本使用的较多,但这个改动使得我们有足够的理由重新考虑在java7中使用String.intern()。
  • java8元空间,字符串在堆。
  • StringTable为什么要调整?
    • 1.永久代size默认比较小
    • 2.永久代垃圾回收频率低

3. String的基本操作

StringTest4:debug里的memory查看String的count:

public class StringTest4 {
    public static void main(String[] args) {
        //String:3703
        System.out.println(); //3704
        System.out.println("1");//3705
        System.out.println("2");//3706
        System.out.println("3");//3707
        System.out.println("4");//3708
        System.out.println("5");//3709
        System.out.println("6");//3710
        System.out.println("1");//3710
        System.out.println("2");//3710
        System.out.println("3");//3710
        System.out.println("4");//3710
        System.out.println("5");//3710
        System.out.println("6");//3710
    }
}

param.toString()创建了字符串java.lang.Object@1c53fd30,放在StringPool中。

public class Memory {
    public static void main(String[] args) {
        int i =1;
        Object object = new Object();
        Memory memory = new Memory();
        memory.foo(object);//java.lang.Object@1c53fd30
    }
    private void foo(Object param){
        String str = param.toString();
        System.out.println(str);
    }
}

4. 字符串拼接操作

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

操作示例:

public class StringTest5 {
    public void test1(){
        String s1 = "a"+"b"+"c";
        String s2 = "abc"; //"abc"一定是放在常量池中的。
        /*
        最终.java编译成.class,再执行.class
        String s1 = "abc";
        String s2 = "abc";
         */
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
        System.out.println("----------test1----over-----");
    }
     public void test2(){
        String s1 = "javaEE";
        String s2 = "hadoop";

        String s3 = "javaEEhadoop";
        String s4 = "javaEE"+"hadoop"; //编译期优化,同test1
        // 如果拼接符号的前后出现了变量,则需要在堆空间中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"
         //如果存在,则返回常量池中的地址,如果不存在,则加载一份。
         String s8 = s6.intern();
         System.out.println(s3 == s8); //true
         System.out.println("----------test2----over-----");
     }
    public void test3(){
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        /*
         如下的s1+s2的执行细节:
         1. StringBuilder s = new StringBuilder();
         2. s.append("a");
         3. s.append("b");
         4. s.toString(); --> 约等于 new String("ab");
         
         jdk5.0之后是StringBuilder,之前是StringBuffer
         */
        String s4 = s1+s2;
        System.out.println(s3 == s4); //false
    }
    /*
    1.  字符串拼接操作并一定使用的是StringBuilder!
        如果拼接符号左右两边都是字符串常量(String s3 = "a"+"b";)或常量引用(String s4 = s1+s2;),则仍然使用编译器优化
    2.  针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final建议使用上。
    */
    public void test4(){
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1+s2;
        System.out.println(s3 == s4); //true
        System.out.println("----------test4----over-----");
    }

    public void test5(){
        String s1 = "javaEEhadoop";
        String s2 = "javaEE";
        String s3 = s2+"hadoop";
        System.out.println(s1 == s3); //false

        final String s4 = "javaEE";
        String s5 = s4+"hadoop";
        System.out.println(s1 == s5); //true
        System.out.println("----------test5----over-----");
    }

    public static void main(String[] args) {
        StringTest5 t = new StringTest5();
        t.test1();
        t.test2();
        t.test3();
        t.test4();
        t.test5();
    }
}

test3()的字节码

 0 ldc #17 <a>    //常量池中的"a"
 2 astore_1		// 存到局部变量1
 3 ldc #18 <b>    //常量池中的"b"
 5 astore_2	   // 存到局部变量2
 6 ldc #19 <ab>    //常量池中的"ab"
 8 astore_3		// 存到局部变量3
 9 new #11 <java/lang/StringBuilder>    // 9-13 :new StringBuilder() -->stringBuilder
12 dup
13 invokespecial #12 <java/lang/StringBuilder.<init>> 
16 aload_1		// 取局部变量1的“a”
17 invokevirtual #13 <java/lang/StringBuilder.append>		//StringBuilder.append(“a”)
20 aload_2     // 取局部变量2的“b”
21 invokevirtual #13 <java/lang/StringBuilder.append>		//stringBuilder.append(“b”)
24 invokevirtual #14 <java/lang/StringBuilder.toString> //stringBuilder.toString()
27 astore 4 // "ab" 存到局部变量4
29 getstatic #3 <java/lang/System.out>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #4 <java/io/PrintStream.println>
46 return

test4()的字节码

 0 ldc #17 <a>
 2 astore_1
 3 ldc #18 <b>
 5 astore_2
 6 ldc #19 <ab>
 8 astore_3
 9 ldc #19 <ab>  //直接从常量池中读取
11 astore 4
13 getstatic #3 <java/lang/System.out>
16 aload_3
17 aload 4
19 if_acmpne 26 (+7)
22 iconst_1
23 goto 27 (+4)
26 iconst_0
27 invokevirtual #4 <java/io/PrintStream.println>
30 getstatic #3 <java/lang/System.out>
33 ldc #21 <----------test4----over----->
35 invokevirtual #7 <java/io/PrintStream.println>
38 return

体会执行效率,通过StringBuilder.append()方式效率远高于String的字符串拼接方式

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

改进的空间:如果基本确定要前前后后添加的字符串不高于某个限定值时,建议预先分配容量: new StringBuilder(capacity);

public void test6(){

        long start = System.currentTimeMillis();

//        method1(100000);      //"花费的时间为"5782
        method2(100000);   //"花费的时间为"14
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为" + (end - start));
        System.out.println("----------test6----over-----");
    }

    private void method2(int highlevel) {
        StringBuilder src = new StringBuilder();
        for (int i = 0; i < highlevel; i++) {
            src.append("a");
        }
    }

    private void method1(int highlevel) {
        String src = "";
        for (int i = 0; i < highlevel; i++) {
            src+="a";
        }
    }

5. intern()的使用

源码

(是一个本地方法):

/**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     * @jls 3.10.5 String Literals
     */
    public native String intern();

如果不是双引号声明的String对象,可以使用String提供的intern()方法:intern()方法会从字符串常量池中查询当前字符串是否存在。若不存在则将当前字符串放入常量池中。
比如:String myInfo = new String("I love atguigu").intern();

也就是说,如果在任意字符串上调用intern()方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true:
("a"+"b"+"c").intern() == "abc"

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

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

  1. String s = “shkstart”;
  2. 调用.intern()方法
    String s = new String("shkstart").intern();
    String s = new StringBuilder("shkstart").toString().intern();//关键在.intern(),前面可以改变。

面试题

  • new String(“ab”)会创建几个对象?
    答:如果字符串常量池中没有"ab",两个,堆中一个,字符串常量池中一个;如果有,一个,堆中。
  • new String(“a”)+new String(“a”)会创建几个对象?
    答:对象1:new StringBuilder;对象2:new String(“a”);对象3:常量池中的"a";对象4:new String(“b”);对象5:常量池中的"b";对象6(StringBuilder.toString()中创建):new String(“ab”);没有往常量池中添加"ab"
  • 下面代码:
    • jdk6 :false,false;
    • jdk7+ :false,true;
public class StringInternTest {
    public static void main(String[] args) {
        String s = new String("1");
        //指向堆中地址,此时字符串常量池中已有"1"。
        s.intern();
        String s2 = "1";//指向字符串常量池的"1"。
        System.out.println(s == s2);//false

        String s3 = new String("1")+new String("1");
        //执行完上一行代码,字符串常量池中没有"11";
        s3.intern();//在字符串常量池中生成"11";在jdk6中创建了一个新的对象"11",也就有新的地址
                    //jdk7中:字符串常量池中记录的是堆空间new String("11")的地址,即s3。
        String s4 = "11";
        System.out.println(s3 == s4); // jdk6:false;jdk7/8:true
    }
}

拓展1:将上面代码改为:

String s3 = new String("1")+new String("1");
        String s4 = "11";
        s3.intern();
        System.out.println(s3 == s4);//false

拓展2:
jdk6: true,false
jdk7+ : true,true

String s = new String("a")+new String("b");
String s2 = s.intern();
System.out.println(s2 == "ab");
System.out.println(s == "ab");

jdk6,7,8: true,false

String x = "ab";
String s = new String("a")+new String("b");
String s2 = s.intern();
System.out.println(s2 == x);//true
System.out.println(s == x);//false

拓展3:

String s1 = new String("a")+new String("b");
s1.intern();
String s2 = "ab";
System.out.println(s1==s2);//true
String s1 = new String("ab");//运行后字符串常量池会生成"ab"
s1.intern();
String s2 = "ab";
System.out.println(s1==s2);//false

intern()效率

对于程序中大量存在的字符串,尤其存在很多重复 字符串时,使用 intern()占用空间更小,里面的String实例数要少一些。

public class StringInternTest2 {
    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])); //1996
            arr[i] = new String(String.valueOf(data[i%data.length])).intern(); //3299
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间:" + (end - start));

        try {
            Thread.sleep(1000000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

6. StringTable的垃圾回收

jvm参数设置
-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails

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

打印结果:

[0.002s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
[0.027s][info   ][gc,heap] Heap region size: 1M
[0.032s][info   ][gc     ] Using G1
[0.032s][info   ][gc,heap,coops] Heap address: 0x00000000ff000000, size: 16 MB, Compressed Oops mode: 32-bit
[0.272s][info   ][gc,heap,exit ] Heap
[0.272s][info   ][gc,heap,exit ]  garbage-first heap   total 16384K, used 2048K [0x00000000ff000000, 0x00000000ff100080, 0x0000000100000000)
[0.272s][info   ][gc,heap,exit ]   region size 1024K, 3 young (3072K), 0 survivors (0K)
[0.272s][info   ][gc,heap,exit ]  Metaspace       used 5191K, capacity 5256K, committed 5376K, reserved 1056768K
[0.272s][info   ][gc,heap,exit ]   class space    used 460K, capacity 492K, committed 512K, reserved 1048576K
SymbolTable statistics: 
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     23757 =    570168 bytes, avg  24.000
Number of literals      :     23757 =    968200 bytes, avg  40.754
Total footprint         :           =   1698456 bytes
Average bucket size     :     1.187
Variance of bucket size :     1.186
Std. dev. of bucket size:     1.089
Maximum bucket size     :         8
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      3400 =     81600 bytes, avg  24.000
Number of literals      :      3400 =    270368 bytes, avg  79.520
Total footprint         :           =    832072 bytes
Average bucket size     :     0.057
Variance of bucket size :     0.057
Std. dev. of bucket size:     0.238
Maximum bucket size     :         3

7. G1中的String去重操作

  • 背景:对很多java应用做的测试得出以下结果:
    • 堆存活数据集合里面String对象占了25%
    • 堆存活数据集合里重复的String对象占13.5%
    • String对象的平均长度为45
  • 很多大规模的java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,存活数据集合不多25%是String对象。更进一步说,这里面差不多一半String对象是重复(equals)的,堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动连续对重复的String对象进行去重,这样就能减少内存的浪费。
  • 实现
    • 当垃圾收集器工作的时候,会访问堆上存活的对象,对每一个访问的对象都会检查是否是候选的要去重的String对象。
    • 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
    • 使用一个hashtable来记录所有的被String对象引用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
    • 如果存在,String对象会被调整引用那个数组,释放对原来数组的引用,最终会被垃圾收集器回收掉。
    • 如果查找失败,char数组会被插入到hashtable,这样以后就可以共享这个数组了。
  • 命令行选项
    • UseStringDeduplication (bool):开启String去重,默认不开启,须手动开启。
    • PrintStringDeduplicationStatistics:打印详细的去重统计信息
    • StringDeduplicationAgeThreshold (uintx)达到这个年龄的String对象被认为是去重的候选对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值