JVM基础之--字符串常量池

193 阅读6分钟

String的基本特性

  • String是字符串,声明为final类不可被继承,实现Serializable接口(支持序列化),实现Comparable接口(可比大小),实现CharSequence接口,是不可变的;
    public final class String implements java.io.Serializable, Comparable<String>, CharSequence
    
  • JDK8内部定义了final char[] value用于存储字符串数据,JDK9类型改为byte[];
  • 存储在字符串常量池(常量池中不存在两个相同的字符串);
  • String有不可变性,对String的重新赋值实际上底层是重新new一个String,因为String底层是数组,数组在编译的时候长度已经确定了;
  • 通过字面量的方式给String赋值,String将创建在字符串常量池中(String str = "abc";) ,返回常量池中的对象
  • 通过new的构造方法也会在常量池中创建对象,同时还会再堆空间中创建对象,但是会返回堆空间中的对象;

StringPool(字符串常量池)

StringPool简介

StringPool底层是一个固定大小的HashTable(数组+链表)

设置StringPool的HashTable长度:-XX:StringTableSize

jdk6的StringPool默认长度时1009(没有限制要求)

jdk7的StringPool默认长度时60013(没有限制要求)

jdk8的StringPool默认长度是60013(最小值限制为1009)

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

注意:StringPool的String多了就会产生Hash冲突,调用String.intern时性能下降。

StringPool的内存分配

Java语言中的8种数据类型和String类,为了在运行的时候响应更快,更节省内存,提供了一种常量池的概念,8种数据类型的常量池是系统协调的,String类型的常量池比较特殊,有两种创建方法:

  • 字面量声明(双引号括起来的都可以算字面量声明:System.out.printf("abc"); 或 String str = "abc";),会直接创建在字符串常量池中
  • 非字面量声明,使用String.intern()方法加入字符串常量池中,返回常量池中的对象

StringPool的位置

StringPool的位置在jdk6和jdk7之间有重大的位置调整,调整结构图如下:

image.png

image.png

到了JDK8或以上方法区的实现方式变为元空间,其他的没有改变。

调整原因

调整字符串常量池最重要的原因有两点:

  1. 在JDK8或以前,方法区的实现方式是永久代,而永久代是在JVM里面分配的,32位机器永久代默认最大空间是64M,64位机器永久代默认最大空间是82M,可见是比较小的,对于程序中大量应用的String字符串,容易出现OOM;
  2. 永久代属于方法区的实现方式,垃圾回收的频率很低;

String的拼接操作

拼接操作的特点

  1. 常量的拼接(字面量) 结果是只放在字符串常量池中的,原理是编译期优化;
    public void stringTest1(){
        String s1 = "a" + "b" + "c";
        String s2 = "abc";
        /**
         * 在java文件编译成class文件后,会对s1和s2进行编译期优化
         * 打开编译后的class文件可以看到
         * String s1 = "abc";
         * String s2 = "abc";
         */
        System.out.println(s1 == s2);//true
        System.out.println(s1.equals(s2));//true
    }
    
  2. 只要其中一个是变量(包括两个或以上都是变量),结果就在堆中(常量池外,且常量池不会有);
    public void stringTest2(){
        String s1 = "Java";
        String s2 = "Hotspot";
        String s3 = "JavaHotspot";
        String s4 = "Java" + "Hotspot";
        String s5 = s1 + "Hotspot";
        String s6 = "Java" + 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
    }
    

字符串拼接的底层原理

字面量拼接

字面量拼接也就是常量拼接,可以以拼接特点的第一点的代码来说明,主要是编译器的优化,且结果只存在字符串常量池。

public void stringTest1(){
    String s1 = "a" + "b" + "c";
    String s2 = "abc";
    /**
     * 在java文件编译成class文件后,会对s1和s2进行编译期优化
     * 打开编译后的class文件可以看到
     * String s1 = "abc";
     * String s2 = "abc";
     */
    System.out.println(s1 == s2);//true
    System.out.println(s1.equals(s2));//true
}

变量拼接

变量拼接(只要其中有一个或以上的变量),结果就会在堆中,而不会在字符串常量池中。

public static void stringTest3(){
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    /**
     * s1 + s2 的执行细节如下:
     *  1. StringBuilder s = new StringBuilder();
     *  s.append("a");
     *  s.append("b");
     *  s.toString(); --> 约等于 new String("ab")
     */
    String s4 = s1 + s2;
    System.out.println(s3 == s4);//false
}

以拼接特点的第二点来说明,s4是由变量拼接而成的,所以结果不会再常量池中 (StringBuilder的toString方法中new的String是没有放入字符串常量池中的),而是在堆中,所以s3==s4false

建议

对于final修饰的类,方法,基本数据类型,引用数据类型的量的结构时,可以使用final就使用final,因为在类加载的链接的第二个阶段准备,final修饰的量就直接显示初始化了。

效率

使用拼接符号进行拼接底层其实是使用StringBuilder调用append方法进行拼接,每一次的拼接都要new一个StringBuilder和String,内存占用大,而且GC的时候花费更多的时间。而直接使用StringBuilder进行拼接,效率会比使用拼接符号高很多。

StringBuilde使用优化建议:创建StringBuilder的时候调用其构造函数,减少使用时的扩容,前提时需要确定StringBuiler的内存空间

StringBuiler s = new StringBuilder(highLevel);//new char[highLevel]

String的intern()方法

方法说明

调用intern()方法,会通过equals方法比较,查询字符串常量池中有没有相同的String对象

  • 如果有,返回该相同对象字符串常量池中的地址
  • 如果没有,创建一个对象在常量池中,返回字符串常量池中的对象(JDK6和JDK7或以上的有区别)

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

  • 字面量定义:String s = "abc";
  • 调用intern()方法:
    String s = new String("abc").intern();
    String s = new StringBuilder("abc").toString().intern();
    

JDK6和JDK7/8或以上intern方法的区别

JDK6:将字符串对象放入字符串常量池中

  • 如果字符串常量池有,则不会放入,返回已有的池中的对象

  • 如果字符串常量池没有,则深拷贝一份对象,放入池子中,并返回池子中的对象地址(地址和堆中的不一样)

JDK7/8或以上:将字符串对象放入字符串常量池中

  • 如果字符串常量池中有,则不会放入,返回已有的池中的对象

  • 如果字符串常量池中没有,则会把对象的地址复制一份,放入池中,返回对象引用地址(地址和堆中的一样)

intern()方法的目的

对于大量的重复的字符串对象,使用intern方法可以使返回的对象指向常量池中的字符串对象,而new出来的字符串对象会因为没有使用被回收,节省空间

G1垃圾回收器中String的去重操作:去String底层char/byte数组的重复,但是要保证String的不变性