JVM--13---StringTable

Posted 高高for 循环

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM--13---StringTable相关的知识,希望对你有一定的参考价值。

文章目录


StringTable

String的基本特性

API–05–String–特性

JDK 9将字符存储在 char 数组

  • String 类的当前实现将字符存储在 char 数组中,每个字符使用两个字节(16位)。从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁字符。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部 char 数组中有一半的空间将不会使用。
  • 我们建议改变字符串的内部表示 Class 从 UTF-16 字符数组到字节数组+一个 encoding-flag 字段。新的 String 类将根据字符串的内容存储编码为 ISO-8859-1/Latin-1(每个字符一个字节)或 UTF-16 (每个字符两个字节)的字符。编码标志将指示使用哪种编码。

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 ex = new StringExer();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str);
        System.out.println(ex.ch);
    }
}

改成 this.str = “test ok”;

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

String 的 String Pool 是一个固定大小的 Hashtable

String的内存分配

  • Java 6 及以前,字符串常量池存放在永久代
  • Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到 Java 堆内
  • Java 8 元空间,字符串常量在堆

Java 8 元空间,字符串常量在堆

  1. 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
  2. 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在 Java 7 中使用 String.intern()。


为什么 StringTable 从永久代调整到堆中

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

String 的基本操作

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

案例解析:

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

字符串拼接操作

  • 常量与常量的拼接结果在常量池,原理是编译期优化
  • 常量池中不会存在相同内容的变量
  • 只要其中有一个是变量,结果就在堆中。变量拼接的原理是 StringBuilder
  • 如果拼接的结果调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
  @Test
    public  void test1() {
        String s1 = "a" + "b" + "c";  // 得到 abc的常量池
        String s2 = "abc"; // abc存放在常量池,直接将常量池的地址返回
        /**
         * 最终java编译成.class,再执行.class
         */
        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";//编译期优化
        //如果拼接符号的前后出现了变量,则相当于在堆空间中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
    }

从上述的结果我们可以知道:

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

2. 而调用intern方法,则会判断字符串常量池中是否存在JavaEEhadoop值,如果存在则返回常量池中的值,否者就在常量池中创建

字符串拼接----底层原理

拼接操作的底层其实使用了 StringBuilder

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

s1 + s2 的执行细节

① StringBuilder s = new StringBuilder();
② s.append(“a”)
③ s.append(“b”)
④ s.toString() --> 约等于 new String(“ab”)

在 JDK 5 之后,使用的是 StringBuilder,在 JDK 5 之前使用的是 StringBuffer

final 修饰,则是从常量池中获取

  • 如果使用的是 final 修饰,则是从常量池中获取。所以说拼接符号左右两边都是字符串常量或常量引用则仍然使用编译器优化。即非StringBuilder的方式。
public static void test4() {
    final String s1 = "a";
    final String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3 == s4);// true
}

拼接操作和append性能对比

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

public static void method2(int highLevel) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < highLevel; i++) {
        sb.append("a");
    }
}

方法1耗费的时间:4005ms,方法2消耗时间:7ms

原因分析:

StringBuilder的append的方式

  • 自始至终只创建一个StringBuilder的对象

字符串拼接的方式:

  1. 每一次拼接,都要创建一次StringBuilder对象和 调用toString时候创建的String对象
  2. 内存中由于创建了较多的StringBuilder和String对象,内存占用过大,如果进行GC那么将会耗费更多的时间。

改进的空间

我们使用的是 StringBuilder 的空参构造器,默认的字符串容量是16,然后将原来的字符串拷贝到新的字符串中,

  • 我们也可以默认初始化更大的长度,减少扩容的次数

结论:

  1. 通过 StringBuilder 的 append() 方式添加字符串的效率,要远远高于 String 的字符串拼接方法
  2. 因此在实际开发中,我们能够确定,前前后后需要添加的字符串不高于某个限定值,那么建议使用构造器创建一个阈值的长度

intern()的使用

string.intern()

intern() 是一个 native 方法,调用的是底层 C 的方法

  1. 在调用 intern() 方法时,如果池中已经包含了由 equals(object),方法确定的与该字符串对象相等的字符串,则返回池中的字符串。
  2. 否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。

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

(“a”+“b”+“c”).intern()==“abc”

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

经典例题:

题目1:

String s = new String(“abc”);方式创建对象,在内存中创建了几个对象?

两个:

  • 一个是堆空间中new结构 ==> 对象名 s 类型为string
  • 另一个是char[]对应的常量池中的数据"abc" ==> 对象名value 类型为char[]

解析:

  1. 一个是 new出来 string类型 ,对象名为s的对象,这个对象的地址值指向堆空间.
  2. 另一个是string对象里面属性 :private final char value[ ];char数组类型的,属性名叫value.这也是个对象,且这个对象地址值指向方法区对应的常量池中的数据

题目2:

String str =new String(“a”)+new String(“b”);,在内存中创建了几个对象?

    @Test
    public  void test2() {
       String str =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"

题目3:

  • main方法测, @Test不准

    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.intern();
        String s4 = "11";
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }

分析

  • String s = new String(“1”); s指向new string() 对象地址值
    s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
    String s2 = “1”; s2 指向字符串常量池中"1"

  • String s3 = new String(“1”) + new String(“1”); 通过案例2 得知s3变量记录的地址为:new String(“11”)

如何理解 s3.intern()

s3.intern();//在字符串常量池中生成"11"。

  • jdk6:创建了一个新的对象"11",也就有新的地址。
  • jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String(“11”)的地址

String s4 = “11”;//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址


题目4:

   public static void main(String[] args) {
        String s3 = new String("1") + new String("1");
        String s4 = "11";
        s3.intern();
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }

题目3:

jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String(“11”)的地址

小结 intern()的使用

题目5:

JDK 6

JDK7,8

先常量池中定义

intern() 的空间效率测试

我们通过测试一下,使用了 intern 和不使用的时候,其实相差还挺多的

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

        try {
            Thread.sleep(1000000);
        } catch (Exception e) {
            e.getStackTrace();
        }
    }
}
  • 花费的时间为:3080
  • 花费的时间为:949

内存情况


intern() 节省内存空间—原理:

  • string调用 intern(),会返回常量池中string对象的地址
  • 那么new 出来的 很多重复的string对象,就没有指向了,这是就会被垃圾回收器回收.
  • 那么那些重复的string对象,都会被慢慢回收,所以内存空间会节省下来

结论:

  • 对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用 intern() 方法能够节省内存空间
  • 大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用 intern() 方法,就会很明显降低内存的大小。

符串都调用 intern() 方法,就会很明显降低内存的大小

StringTable的垃圾回收

G1 中的 String 去重操作

  • 注意这里说的重复,指的是在堆中的数据,而不是常量池中的,因为常量池中的本身就不会重复

背景:

实现

命令行选项

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

以上是关于JVM--13---StringTable的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——CSS选择器

谷歌浏览器调试jsp 引入代码片段,如何调试代码片段中的js

片段和活动之间的核心区别是啥?哪些代码可以写成片段?

VSCode自定义代码片段——.vue文件的模板

VSCode自定义代码片段6——CSS选择器

VSCode自定义代码片段——声明函数