跳至主要內容

JVM (七) 字符串详解

安图新大约 7 分钟

JVM (七) 字符串详解

常量池:

我们前面也一直说常量池有三种:

1:class 文件中的常量池,前面我们解析 class 文件的时候解析的就是,这是静态常量池。在硬盘上。

2:运行时常量池。可以通过 HSDB 查看,是 InstanceKlass 的一个属性:ConstantPool *_constants。在方法区或者说在元空间中(JDK1.8+)

可以通过 HSDB 查看,HSDB 的使用可以看 JVM 第一篇中的介绍。

  

3:字符串常量池。底层是 String Pool--StringTable--HashTable。在堆区。

注意:并不是所有的字符串都会在字符串常量池里。

String 是怎么存储的?

在 java 中我们的 String 对象存储的字符串都是在其内部的一个 char 数组上的。

 
 
 
 

我们看到两个不同的变量,以不同的创建方式创建,字符串一样,但是字符串变量里的 value 数组属性地址竟然是一样的? 是不是很神奇。这就牵涉到 JVM 里面

是怎么存储字符串的问题了。还有就是两个变量的 hashcode 值也是一样的,这是因为 String 重写了 hashcode 方法,hash 值只和字符串的内容也就是 value 有关,所以是一样的。

JVM 中的 String 是怎么存储的呢?

在 JVM 中,使用 StringTable 来存储 String 的当然也有些不是通过 StringTable 存储的,这个后面说明。StringTable 继承 HashTable,也就是字符串在 JVM 中是 key-value 形式存储的。数据结构也就是数组+链表。

在 openJDK 中的 symbolTable.cpp 中如下方法:

 
 

key:

是通过 1 中的方式生成的。1)根据字符串以及字符串的长度计算出 hashvalue.2)根据 hashvalue 计算出 index,这个 index 就是 key。也就是数组的下标,在这里称为 bucket(桶)默认桶的数量为 60013 个。

可以通过-XX:StringTableSize=2000参数来调整桶的大小。

value:

key 计算出了 bucket 的位置,value 的值就是 2 中生成的 HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());

它是将 Java 中 String 类的实例 instanceOopDesc 封装成了 HashtableEntry,再存储起来的。

这里补充下,在第一篇 JVM 中已经提到了 oop-klass 体系。这里再说明下:

Oop: java 中对象在 JVM 中的存在形式。klass 是 java 中的类在 JVM 中存在的形式。

 
 

通过 idea 我们可以看到在创建 String 过程中都创建了些什么内容

实例

我们从 idea 中接着看上面的例子。看下创建字符串过程中到底创建了那些内容。

public static void main(String[] args) {
        test1();
    }
    public static void test1(){
        String s1="1";
        String s3="1";
        String s2=new String("1");
        System.out.println(s1==s3);
        System.out.println(s1==s2);

    }

以 Debug 的方式调试,在控制台最右上角有个 Memory View,可以实时看到每一步创建了那些对象,创建了几个。

 
 

第一行执行完:我们看到 char[] ,String 各新增了一个。

 
 

第二行执行完:char[],String 一个都没新增,很神奇吧,别慌,执行完。

 
 

第三行执行完:只新增了一个 String。

 
 

两个比较结果:s1 和 s3 的地址是一样的。s2 是不同的地址。

 
 

为什么会出现上面看到的结果呢?关键看下图:

1)如果是一个 char[]数组类型数据 ,它的对象在 JVM 中是 typeArrayOopDesc 形式的。

 
 

2:直接双引号创建一个字符串:按照上面说的,字面字符串会在堆里有一个 String 对象,String 对象里有一个 char[]数组对象,把 String 对象对应的 instanceOopDesc 封装成 HashTableEntry 然后把 HashTableEntry 放入常量池中。s1 只是引用这个 String 对象。

 
 

3:两个双引号:当 s2 创建“11”字面字符串时,会首先判断常量池是否有这个字符串如果有的话会直接返回这个字符串的 instanceOopDesc。所以 s1,s2 指向的是同一个块地址。

如果没有的话会创建一个像 2 中的那样。

 
 

4:通过 new String 创建字符串:new 操作会在堆里创建一个 String 对象,这个 String 对象的 char 数组还是指向 typeArrayOopDesc,如果字符串常量池中已经存在了当前字符串,

还是会指向已经存在的地址。

因此可以看到上面举得例子,s1,s2,s3 变量中的 char 数组的内存地址都是一样的!!

 
 

5:创建两个 new String 方式的字符串,常量池中还是只有一个,但是两个 s1,s2 地址是不一样的,但是其下的 char 数组还是会指向同一个 typeArrayOopDesc。

 
 

字符串拼接

  public static void test1(){
        String s1="1";
        String  s2="2";
        String s3=new String("3");
        String s6="12";
        String s7="13";
        String s4=s1+s2;
        String s5=s1+s3;
    }

我们首先看下字符串拼接底层是怎样实现的。通过 javap -c TestString.class 可以查看字节码指令。或者直接通过 idea 查看.class 文件

 
 

我们看到 String s4=s1+s2; String s5=s1+s3; 底层都是通过 StringBuilder#append 来拼接之后再 toString 得到的。但是不仅仅只有这一点区别!

我们继续看 StringBuilder#toString 方法。发现是调用了 new String(value, 0, count); 的构造方法。

 
 

我们通过 Debug 看下,通过拼接得到的字符串有什么不一样的地方?

首先看下 String s6="12"; String s4=s1+s2; 的区别,s1+s2 得到的字符串也是"12", 这里 char 数组地址竟然不一样了!!!

我们上面知道常量池中如果已经有了这个字符串,下面创建同样的字符串的时候都是从常量池中获取,char 数组的地址都是一样的。这里竟然不一样了!

这就是拼接字符串的不同之处,拼接出来的字符串并没有从常量池中获取,创建出来的字符串也不会放入字符串常量池中,s6 是常量池中的字符串,s4 里面的 char 数组就是普通的堆里面的数组。s5 拼接的字符串也是这样的。

 
 

我们这里把这个这个构造函数和常量字符串构建单独拉出来看下。

 
 

第一行执行完:String,char[] 各新增一个。

 
 

第二行执行完:新增了两个 String,一个 char[]数组 ,而且看到 s1,s3 字符串虽然一样的,但是 char[] 却不再一样了。这也就是 s1 字符串并不在常量池中,s3 会把字符串放入常量池中。

 
 

虽然 String s=s1+s2 这种拼接的字符串并不会放到字符串常量池中,但是我们可以调用 String#intern 方法把当前的字符串主动放入字符串常量池中。

我们还是以上面这个例子,加一行代码:

第一行执行完结果:

 
 

第二行执行完结果:没有什么明显的结果

 
 

第三行执行完: 只新增了一个 String 对象,而且 char[] 数组地址是一样的。这是因为 s1.intern()方法,把 s1 的字符串放入常量池中了,s3 创建的时候,只是在堆里再创建一个新的 String 对象就可以了,这个在上面的图解中也说明了。

 
 

还有一种特殊情况我们来看下,有 final 修饰符修饰的字符串之间的拼接情况:

public static void test3(){
       final String s1="1";
       final String s2="2";
       String s3=s1+s2;
       String s4="12";
      System.out.println(s3==s4);
}

因为 s1,s2 是 final 修饰的,在编译阶段就放入了字节码的常量池中,s3 其实在编译阶段已经指向了常量池中的"12"了。

我们通过编译后的 class 也可以看到:所以比较肯定是 true。

 
 
//        只会创建一个String  一个char[],编译的时候就优化成"帅帅"
        String s="帅"+"帅";
//      三个String,三个char数组对象,
 String s2 = "帅" + new String("真帅");
上次编辑于:
贡献者: Andy