• JVM:(十二)StringTable


    12.1 String的简介

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

    • String声明为final的,不可被继承

    • String实现了Serializable接口:表示字符串是支持序列化的。

    • String实现了Comparable接口:表示string可以比较大小

    • String在jdk8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]

    • String在jdk9中存储结构变更,底层由char[]数组变为byte[]数组

      • 从许多不同的应用中收集到的数据表明,字符串是堆使用的主要组成部分,此外,大多数字符串对象只包含Latin-1字符。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部字符数组中有一半的空间没有被使用。所以将String类的内部表示方法从UTF-16字符数组改为字节数组加编码标志域。新的String类将根据字符串的内容,以ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的方式存储字符编码。编码标志将表明使用的是哪种编码。

    12.2 String的不可变性

    1. String:代表不可变的字符序列。简称:不可变性

      • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。

      • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

      • 当调用string的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

    2. 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

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

    4. String的String Pool是一个固定大小的Hashtable。如果HashTable的长度较小,且放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。

    5. 使用-XX:StringTablesize可设置StringTable的长度

      • 在jdk6中StringTable是固定的,就是1009的长度,StringTablesize设置没限制

      • 在jdk7中,StringTable的长度默认值是60013,StringTablesize设置没有限制

      • 在JDK8中,设置StringTable长度的话,1009是可以设置的最小值

    12.3 String的内存分配

    在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念,池化资源的功能就是缓存与复用。String的主要使用方法有两种:

    1. 直接使用双引号声明出来的String对象会直接存储在常量池中。
    2. 不是用双引号声明的String对象(new出来的或者toString()生成的),对象存放在堆中。此时可以使用String提供的intern()方法。

    Java 6及以前,字符串常量池存放在永久代。Java 7以后,将字符串常量池的位置调整到Java堆内。

    image-20220917031825262

    image-20220916005321810

    @Test
    public void test1() {
        System.out.print1n("1"); 	//2321
        System.out.println("2"); 	//2322
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10");	//2330
        System.out.println("1"); 	//2321
        System.out.println("2"); 	//2322
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.print1n("6");
        System.out.print1n("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10");	//2330
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    12.4 字符串拼接操作

    12.4.1 字符串拼接规则

    • 常量与常量的拼接结果在常量池,原理是编译期优化,这里常量是指字面量或是final修饰的String常量

    • 常量池中不会存在相同内容的变量

    • 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder

    • 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址

    public static void test1() {
        // 都是常量,前端编译期会进行代码优化
        // 通过idea直接看对应的反编译的class文件,会显示 String s1 = "abc"; 说明做了代码优化
        String s1 = "a" + "b" + "c";  
        String s2 = "abc"; 
    
        // true,有上述可知,s1和s2实际上指向字符串常量池中的同一个值
        System.out.println(s1 == s2); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    public static void test2() {
        String s1 = "javaEE";
        String s2 = "hadoop";
    
        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";    
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2;
    
        System.out.println(s3 == s4); // true 编译期优化
        System.out.println(s3 == s5); // false s1是变量,不能编译期优化
        System.out.println(s3 == s6); // false s2是变量,不能编译期优化
        System.out.println(s3 == s7); // false s1、s2都是变量
        System.out.println(s5 == s6); // false s5、s6 不同的对象实例
        System.out.println(s5 == s7); // false s5、s7 不同的对象实例
        System.out.println(s6 == s7); // false s6、s7 不同的对象实例
    
        String s8 = s6.intern();
        System.out.println(s3 == s8); // true intern之后,s8和s3一样,指向字符串常量池中的"javaEEhadoop"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    public void test6(){
        String s0 = "beijing";
        String s1 = "bei";
        String s2 = "jing";
        String s3 = s1 + s2;
        System.out.println(s0 == s3); // false s3指向对象实例,s0指向字符串常量池中的"beijing"
        String s7 = "shanxi";
        final String s4 = "shan";
        final String s5 = "xi";
        String s6 = s4 + s5;
        System.out.println(s6 == s7); // true s4和s5是final修饰的,编译期就能确定s6的值了
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Trick:

    • 不使用final修饰,即为变量。如s3行的s1和s2,会通过new StringBuilder进行拼接

    • 使用final修饰,即为常量。会在编译器进行代码优化。在实际开发中,能够使用final的,尽量使用

    • s1 + s2实际上是new了一个StringBuilder对象,并使用了append方法将s1和s2添加进来,最后调用了toString方法赋给s4

    12.4.2 三种字符串拼接操作性能对比

    public class Test
    {
        public static void main(String[] args) {
            int times = 50000;
    
            // String
            long start = System.currentTimeMillis();
            testString(times);
            long end = System.currentTimeMillis();
            System.out.println("String: " + (end-start) + "ms");
    
            // StringBuilder
            start = System.currentTimeMillis();
            testStringBuilder(times);
            end = System.currentTimeMillis();
            System.out.println("StringBuilder: " + (end-start) + "ms");
    
            // StringBuffer
            start = System.currentTimeMillis();
            testStringBuffer(times);
            end = System.currentTimeMillis();
            System.out.println("StringBuffer: " + (end-start) + "ms");
        }
    
        public static void testString(int times) {
            String str = "";
            for (int i = 0; i < times; i++) {
                str += "test";
            }
        }
    
        public static void testStringBuilder(int times) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < times; i++) {
                sb.append("test");
            }
        }
    
        public static void testStringBuffer(int times) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < times; i++) {
                sb.append("test");
            }
        }
    }
    // 结果
    String: 7963ms
    StringBuffer: 4ms
    StringBuilder: 1ms
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    结论:String拼接方式的时间是StringBuilder.append方式的约8000倍,StringBuffer.append()方式的时间是StringBuilder.append()方式的约4倍。

    所以:在实际开发中,对于需要多次或大量拼接的操作,若不考虑线程安全问题,应该尽可能使用StringBuilder进行append操作。

    此外,StringBuiler底层是通过动态数组实现的,涉及到扩容,所以我们如果提前知道需要拼接String的长度,就应该直接使用带参构造器指定容量,能够减少扩容的次数,以提升字符串运行效率

    12.5 intern()的使用

    12.5.1 intern()简介

    1. 当调用intern方法时,如果字符串常量池已经包含了一个与这个String对象值相等的字符串,那么常量池中的字符串会被返回。否则,这个String对象被添加到常量池中,并返回池中该String对象的引用。
    2. 对于任何两个字符串s和t,当且仅当s.equals(t)为真时,s.intern() == t.intern()为真。
    3. 所有字面量和以字符串常量表达式都是interned。
    4. intern()是一个native方法,调用的是底层C的方法
    5. 通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。

    12.5.2 intern()的使用:JDK6 vs JDK7/8

    /**
     * ① String s = new String("1")
     * 创建了两个对象
     * 		堆空间中一个new对象
     * 		字符串常量池中一个字符串常量"1"(注意:此时字符串常量池中已有"1")
     * ② s.intern()由于字符串常量池中已存在"1"
     * s  指向的是堆空间中的对象地址
     * s2 指向的是堆空间中常量池中"1"的地址
     * 所以不相等
     */
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s==s2); // jdk1.6 false jdk7/8 false
    
    /*
     * ① String s3 = new String("1") + new String("1")
     * 执行StringBuilder的toString()方法,本质是new String("11")
     * 但是与直接new String("11")不同,常量池中并不生成字符串"11";
     * ② s3.intern()
     * 由于此时常量池中并无"11",所以把s3中记录的对象的地址存入常量池
     * 这里jdk6和jdk7有区别:
     *		在jdk6中,会直接复制新的"11"对象放入常量池
     *		在jdk7及以后,会直接将s3的引用放入常量池
     */
    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3==s4); //jdk1.6 false jdk7/8 true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    总结:str.intern()执行细节:

    JDK1.6中

    • 如果串池中有str,则并不会放入。返回常量池中的str对象的地址

    • 如果没有,会把str对象复制一份,放入常量池,并返回常量池中的str对象地址

    JDK1.7起

    • 如果串池中有str,则并不会放入。返回常量池中的str对象的地址

    • 如果没有,则会把堆中str对象的引用地址复制一份,放入常量池,并返回常量池中的引用地址

    举例1:

    image-20220916022434529
    在这里插入图片描述

    举例2:
    img

    12.5.3 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++) {
                // 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 (Exception e) {
                e.getStackTrace();
            }
        }
    }
    
    // 运行结果
    不使用intern:7256ms
    使用intern:1395ms
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    结论:对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省内存空间。

    大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用intern()方法,就会很明显降低内存的大小。

    上述代码intern()之后,原来new String()出来的对象就会变成垃圾,将会被回收;此外若字符串常量池占用过大,也会发生GC。

    12.6 G1 GC的String去重

    背景:对许多Java应用(有大的也有小的)做的测试得出以下结果:

    • 堆存活数据集合里面string对象占了25%

    • 堆存活数据集合里面重复的string对象有13.5%

    • string对象的平均长度是45

    许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象。更进一步,这里面差不多一半string对象是重复的,重复的意思是说: stringl.equals(string2)= true。堆上存在重复的String对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的string对象进行去重,这样就能避免浪费内存。

    实现原理

    1. 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象
    2. 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。
    3. 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
    4. 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
    5. 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。
  • 相关阅读:
    keepalived高可用,nginx+keepalived+apache架构的实现
    原来还可以客户端负载均衡
    FL Studio2023水果完整中文版音乐制作软件
    计算机毕业设计之java+javaweb的面向学生成绩分析系统
    小程序列表下拉刷新和加载更多
    固定交付项目适用敏捷开发方式吗?
    win10+cuda+cudnn+anconda+pytorch+pycharm全家桶安装
    电脑监控软件是如何提高员工工作效率的?
    家政预约小程序11新增预约
    《七月集训》(第二十天)——二叉搜索树
  • 原文地址:https://blog.csdn.net/sd_960614/article/details/126900366