JVM学习————字符串常量池(七)

本文详细介绍了JVM中字符串常量池的概念,包括String的不可变性、内存分配方式以及不同JDK版本的变化。讨论了String的基本操作如拼接和intern()方法的使用,并分析了其性能和内存影响。还涉及了StringTable的垃圾回收机制以及G1垃圾收集器中的String去重操作。
摘要由CSDN通过智能技术生成

StringTable

String的基本特性

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

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

  • String 实现了Serializable接口:表示字符串是支持序列化的;实现了Conparable接口:表示String可以比较大小

  • JDK8即以前定义final char[] valuefinal char[] value用于存储字符串数据,JDK9改为byte[](节约了一些空间,同时StringBUffer等内部也修改了)

  • String:代表不可变的字符串序列,简称:不可变性

    • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
    • 当对现有字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
    • 当调用String的replace()修改指定字符串或字符时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
  • 通过字面量声明在字符串常量池中

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

String的String Pool是一个大小固定的HashTable,默认长度是1009.如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表很长,而链表长了以后会造成String.intern时性能大幅下降

使用-XX:StringTableSize可以设置StringTable的长度

jdk6中StringTable时固定的,就是1009的长度,所以如果常量池中字符串过多会导致效率下降很快。StringTableSize设置没有要求

jdk7中StringTable默认是60013。StringTableSize设置没有要求

jdk8开始,设置StringTable长度,1009是可设置的最小值

String的内存分配

常量池就类似一个Java系统级别提供的缓存。8中基本数据类型都是系统协调的,String类型的常量池比较特殊。它主要使用方法有两种

  1. 直接声明String对象会直接存储在常量池中
  2. 使用intern()

Java6之前,字符串常量池放在永久代中

Java7中将字符串常量池放在Java堆中

Java8元空间,字符串常量在堆中

StringTable为什么要调整?

①permSize默认空间比较小②永久代垃圾回收频率低

String的基本操作

public class byte_char {
    public static void main(String[] args) {
        System.out.println();//2228
        System.out.println("1");//2229
        System.out.println("2");//2230
        System.out.println("3");//2231
        System.out.println("1");//2232
        System.out.println("2");//2232
        System.out.println("3");//2232
    }
}

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

public class byte_char {
    public static void main(String[] args) {
        int i = 1;
        Object obj = new Object();
        byte_char byte_char = new byte_char();
        byte_char.foo(obj);
        
    }
    private void foo(Object para) {
        String s = para.toString();
        System.out.println(s);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NH9fuYp4-1621243933067)(JVM.assets/image-20210507095350835.png)]

字符串拼接操作

  1. 常量与处理的拼接结果在常量池,原理是编译期优化
  2. 常量池不会存在相同内容的常量
  3. 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  4. 如果拼接的结果调用inter(),则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
@Test
public void test1() {
    String s1 = "a" + "b" + "c";//编译期优化
    String s2 = "abc";
    System.out.println(s1.equals(s2));//true
    System.out.println(s1 == s2);//true
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JW3eBFQl-1621243933069)(JVM.assets/image-20210507101641778.png)]

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

    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";//编译期优化
    String s5 = s1 + "hadoop";//如果出现了变量,则相当于在堆中间new对象,具体内容为拼接结果
    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

    //s6对应的值,如果在字符串常量池中存在,则返回常量池中对应的地址;如果不存在,则在堆中创建,再将结果放入常量池中
    String s8 = s6.intern();//true
    System.out.println(s3 == s8);
}
@Testpublic void test3() {    String s1 = "a";    String s2 = "b";    String s3 = "ab";    /*    s1 + s2细节如下    ①StringBuilder s = new StringBuilder();    ②s.append("a");    ③s.append("b");    ④s.toString()   --->约等于  new String("ab")    在jdk5.0之后使用的StringBuilder,在jdk5.0之前使用StringBuffer     */    String s4 = s1 + s2;    System.out.println(s4 == s3);//false}
@Testpublic void test4() {    final String s1 = "a";    final String s2 = "b";    String s3 = "ab";    //字符串拼接,不一定是StringBuilder,如果拼接符左右两边都是字符串常量或者是常量引用,则仍然使用编译期优化,非StringBuilder模式    //针对于final修饰基本数据类型,引用数据类型,类,方法若能使用就建议使用final,因为用final修饰在编译期间就会有值,即显示赋值    String s4 = s1 + s2;//此时理解为常量    System.out.println(s4 == s3);//true}

StringBuilder和String的+

体会

  1. 效率
    • 对于String的拼接而言每次循环都要创建String,StringBuilder,
    • 但对于StringBuilder自始至终只会创建一个StringBuilder
  2. 内存
    • 对于String的拼接,每次循环都要创建String,StringBuilder,占用的内存较大;并会有发生GC的可能还需要额外的开销
    • StringBuilder的改进,避免不断扩容;在实际开发中,如果前后添加某个字符串,不高于某个限定值,建议使用有参构造器StringBuilder s = new StringBuilder(highLevel)//new char[highLevel]
@Testpublic void test5() {    long start = System.currentTimeMillis();    //        method1(100000);//花费4692ms    method2(100000);//花费15ms    long end = System.currentTimeMillis();    System.out.println("花费" + (end - start) + "ms");}public void method1(int count) {    String src = "";    for (int i = 0; i < count; i++) {        src = src + "a";//每次循环都要创建String,StringBuilder    }}public void method2(int count) {//自始至终只会创建一个StringBuilder    StringBuilder s = new StringBuilder();    for (int i = 0; i < count; i++) {        s.append("a");    }}

inter()的使用

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

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

public class TestIntern {    public static void main(String[] args) {        String str1 = new String("ab");//在堆空间中创建,并在字符串常量池中也会创建        str1.intern();//此时字符串常量池中已经有"ab"        String str2 = "ab";//指向字符串常量池中        System.out.println(str2==str1); //jdk6/7/8 false        //只会在堆空间中创建,根据字节码分析,StringBuilder中的toString()中的new Stirng("12"),不会在字符串常量池中创建"12"        String str3 = new String("1")+new String("2");        str3.intern();//jdk6   会在字符串常量池中创建新的对象                      //jdk7/8   在字符串常量池中创建指向堆空间的地址值,此时堆和字符串常量池是同一个地址        String str4 = "12";//指向字符串常量池        System.out.println(str3==str4);//jdk6 false【一个指向堆,一个指向字符串常量池】   jdk7/8 true【此时堆和字符串常量池是同一个地址】    }}

因为new String(“ab”)会在堆中创建并去字符串常量池中创建,str2是指向堆中引用,str1指向字符串常量池,因此返回false。但是对于new String(“a”)+new String(“b”)只会在堆中创建,不会在字符串常量池中创建,即字符串常量池中没有"ab",str2.intern()会在字符串常量池中创建,并返回地址,String str1 = “ab”;直接从字符串常量池中中获取,所以二者其实都是从字符串常量池中中获取的,因此返回true

面试题

new Stirng(“ab”)会创建几个对象?会在字符串常量池中中创建"ab"

new String(“a”) + new Stirng(“b”)呢?不会在字符串常量池中创建"ab"

/** *  0 new #2 <java/lang/String>   在堆中创建对象 *  3 dup *  4 ldc #3 <abc>   在字符串常量池中创建 */public class TestString {    public static void main(String[] args) {        String abc = new String("abc");    }}
/** *  0 new #2 <java/lang/StringBuilder>   ①在堆中创建对象StringBuilder *  3 dup *  4 invokespecial #3 <java/lang/StringBuilder.<init>> *  7 new #4 <java/lang/String>   ②在堆中创建对象String * 10 dup * 11 ldc #5 <a>   ③在字符串常量池中创建"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>    ⑤在字符串常量池中创建"b" * * 深入分析:将创建的对象append到StringBuilder中,再调用toString(), * 会再创建一个String对象,即new String("ab"),但是在字符串常量池中并没有生成"ab" */public class TestString {    public static void main(String[] args) {        String s = new String("a") + new String("b");    }}
public class TestIntern1 {    public static void main(String[] args) {        String str3 = new String("1")+new String("1");//在堆中创建一个"11",此时字符串常量池中没有        String str4 = "11";//在字符串常量池中创建"11"        String str5 = str3.intern();//返回字符串常量池中已存在的地址        System.out.println(str3==str4);//false        System.out.println(str4 == str5);//true    }}

intern()的使用:jdk6 vs jdk7/8

  • jdk6中,将这个字符串对象尝试放入串池
    • 如果串池中有,则不会放入。返回已有的串池中的对象的地址
    • 如果没有,会把对象复制一份,放入串池,并返回串池中的对象地址
  • jdk7中,将这个字符串对象尝试放入串池
    • 如果串池中有,则不会放入。返回已有的串池中的对象的地址
    • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
public class TestIntern1 {    public static void main(String[] args) {        String str3 = new String("1")+new String("1");//在堆中创建一个"11",此时字符串常量池中没有        String str5 = str3.intern();        System.out.println(str3=="11");//jdk7/8 true   jdk6 false        System.out.println(str5 == "11");//jdk7/8true  jdk6 true    }}
public class TestIntern2 {    public static void main(String[] args) {        String s1 = new String("ab");//会再字符串常量池中创建    false//        String s1 = new String("a") + new String("b");//不会再字符串常量池中创建   true        s1.intern();//jdk7/8在字符串常量池中创建指向堆地址的值        String s2 = "ab";        System.out.println(s1 == s2);    }}

intern()的效率测试

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

StringTable的垃圾回收

G1中的String去重操作

Java堆中存活的数据集合差不多25%是String对象

堆上存在重复的String对象必然是一种内存的浪费,因此去重操作是对堆操作的

UseStringDeduplication (bool):开启String去重,默认是不开启的,需要手动开启

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值