十三-String Table

本文详细探讨了Java中的String特性和内存分配,包括String的不可变性、字符串常量池、内存区域的变化(从永久代到Java堆)。重点讨论了StringTable的优化,如JDK6和JDK7/8后的intern()方法行为差异,以及G1垃圾收集器的String去重操作,旨在提高内存效率。
摘要由CSDN通过智能技术生成

13.1 String的基本特性

  • String声明为final的,不可被继承
  • String实现了Serializable接口:表示字符串是支持序列化的;实现了Comparable接口:表示String可以比较大小
  • String在jdk8以前内部定义了final char[] value用于存储字符串数据,jdk9改为byte[]
  • String:代表不可变的字符序列。不可变性
public class StringTest1 {

    public static void test1() {
        // 字面量定义的方式,“abc”存储在字符串常量池中
        String s1 = "abc";
        String s2 = "abc";
        System.out.println(s1 == s2);//判断地址,true
        s1 = "hello";
        System.out.println(s1 == s2);//false
        System.out.println(s1);
        System.out.println(s2);
        System.out.println("----------------");
    }

    public static void test2() {
        String s1 = "abc";
        String s2 = "abc";
        // 只要进行了修改,就会重新创建一个对象,这就是不可变性
        s2 += "def";
        System.out.println(s1);//abc
        System.out.println(s2);//abcdef
        System.out.println("----------------");
    }

    public static void test3() {
        String s1 = "abc";
        String s2 = s1.replace('a', 'm');
        System.out.println(s1);//abc
        System.out.println(s2);//mbc
    }

    public static void main(String[] args) {
        test1();
        test2();
        test3();
    }
}

面试题

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);//good
        System.out.println(ex.ch);//best
    }
}

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

  • String的String Pool是一个固定大小的HashTable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表很长,而链表长了会导致调用string.intern性能大幅下降
  • 使用-XX:SttingTableSize设置StringTable长度
  • jdk6中,长度是1009,在jdk7中默认是60013,jdk8后,要求最小值1009

13.2 String的内存分配

  • 在Java语言中有8中基本数据类型和一种比较特殊的类型String。这些类型为了使他们在运行时速度更快、更节省内存,都提供了一种常量池的概念
  • 常量池类似于一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊,主要使用方法有2种
  • 直接使用双引号声明出来的string对象会直接存储在常量池种。String info=“xxxx”
  • 不是双引号声明的string对象,可以使用string提供的intern()方法。

String的内存分配

  1. JDK6以前,字符串常量池存放在永久代
  2. JDK中,将字符串常量池的位置调整到Java堆内
  3. JDK8元空间,字符串常量在堆
    JDK6
    JDK7
    StringTable为什么要调整
    1.permSize永久代大小默认比较小
    2.永久代垃圾回收频率低

13.3 String的基本操作

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

13.4 字符串拼接操作

  • 常量与常量的拼接结果在常量池,原理是编译器优化
  • 常量池中不会存在相同内容的常量
  • 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  • 如果拼接的结果调用inern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
    public static 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
    }

	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); // 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,并返回此对象的地址
        String s8 = s6.intern();
        System.out.println(s3 == s8); // true
    }

底层原理

拼接操作的底层其实是用的StringBuilder
底层
s1+s2的执行细节:
StringBuilder s = new StringBuiler();
s.append(“a”);
s.append(“b”);
s.toString();---->约等于 new String(“ab”)

在JDK5后,使用的是StringBuiler。之前是用的StringBuffer

如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译器优化,即非StringBuiler的方式
针对final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用final的时候建议加上

拼接操作和append性能对比

/*
* 通过StringBuilder的append()方式添加字符串的效率要远高于使用String字符串拼接方式。
* 自始至终只创建了一个StringBuilder对象,由于创建了较多对象,内存占用更大,如果进行GC,花费更多时间
* 在实际开发中,要前后添加的字符串长度不高于某个值,使用构造器StringBuilder(int capacity)
*/
 public static void method1(int highLevel) {//耗时4000ms
        String src = "";
        for (int i = 0; i < highLevel; i++) {
            src += "a"; // 每次循环都会创建一个StringBuilder对象
        }
    }

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

13.5 intern()的使用

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

  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法,intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
  • 如果在任意字符串上调用String.intern方法,其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同

面试题

new String(“ab”)会创建几个对象?
new String(“a”)+new String(“b”)呢?

public class StringNewTest {
    public static void main(String[] args) {
    	//一个对象是new关键字在堆空间创建的
        //另一个对象是字符串常量池中的对象,字节码指令ldc
        String str = new String("ab");

		//对象1,new StringBuiler()
		//对象2,new String("a")
		//对象3,常量池中的”a“
		//对象4,new String("b")
		//对象5,常量池中的”b“
		//对象6,StringBuiler的toString()方法,常量池中没有ab,new String("ab")
         String str2 = new String("a") + new String("b");
    }
}

str字节码指令

 0 new #2 <java/lang/String>
 3 dup
 4 ldc #3 <ab>
 6 invokespecial #4 <java/lang/String.<init>>
 9 astore_1
10 return

str2字节码指令

 0 new #2 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init>>
 7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return

intern()的使用,JDK6 VS JDK7/8

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的地址为new String("11")
//执行完上一行代码后,字符串常量池中,不存在”11“
s3.intern();//在字符串常量池中生成”11“,JDK6中创建一个新对象”11“,JDK7中常量池没有创建新对象,而是指向new的地址
String s4 = "11";//使用的是上一行代码执行时,在常量池生成的”11“的地址
System.out.println(s3 == s4); // JDK6:false    JDK7/8:true

JDK7代码

总结String的intern()的总结
JDK6中,将这个字符串对象尝试放入串池
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,会把此对象复制new一份,放入串池,并返回串池中的对象地址
JDK7起,将这个字符串对象尝试放入串池
如果串池中有,则并不会放入,返回已有的串池中对象的地址
如果没有,会把对象的引用地址复制,放入串池,并返回串池中的引用地址

空间效率测试

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])).intern();
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:" + (end - start));

        try {
            Thread.sleep(1000000);
        } catch (Exception e) {
            e.getStackTrace();
        }
    }
}

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

13.6 StiringTable的垃圾回收

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

13.7 G1中的String去重操作

这里的去重,指的是堆的去重,而不是常量池中的,因为常量池本身就不会重复

背景

  • 堆存活数据集里面String对象占了25%
  • 堆存活数据里里重复的String对象有13.5%
  • String对象的平均长度是45
  • 堆上存在重复的String对象是一种内存浪费。在G1垃圾收集器实现自动持续对重复String对象进行去重

实现

  • 当垃圾收集器工作时,会访问堆上存活的对象。对每一个访问的对象都检查是否时要去重的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、付费专栏及课程。

余额充值