String与常量池

String的两种创建方式

  我们知道创建字符串有两种形式,一种是使用字面量创建字符串,另一种是使用new关键字创建:

1 String str1 = "123"; // 字面量
2 
3 String str2 = new String("123"); // new

  让我们看看下面这段代码

1 String s = new String("1");
2 s.intern();
3 String s2 = "1";
4 System.out.println(s == s2);
5 
6 String s3 = new String("1") + new String("1");
7 s3.intern();
8 String s4 = "11";
9 System.out.println(s3 == s4);

  JDK6输出:false false 

  JDK7输出:false true

  那么是什么导致了两个版本的输出不一致?是常量池。

常量池

  JVM常量池分为三种:

  1. 静态常量池 

  2. 运行时常量池 

  3. 字符串常量池

静态常量池

  静态常量池是class文件的一部分。在class文件结构中有一个叫做constant_pool的部分,这就是静态常量池。静态常量池主要存放两大常量:字面量符号引用。 

  字面量分为文本字符串(例如 String s = "abc"; 其中的"abc"就是文本字符串)与final修饰的变量(包括静态变量、实例变量、局部变量)。

  符号引用包含了类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

  constant_pool中有一个数据类型叫做CONSTANT_Utf8_info,这就是存储UTF-8编码的字符串的数据类型,该类型的结构如下:

struct CONSTANT_UTF8_INFO {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

  其中length的类型是u2,u2是16位无符号整型,所以理论上bytes最大长度是65535,但是由于javac在编译时只允许长度小于65535的字符串,所以如果使用字面量定义字符串,其最大长度是65534(运行时产生的字符串最大长度是-1>>>1)。

  ps:静态常量池的数据类型里面有个叫做CONSTANT_String的类型,一般对他的描述是“存放字符串类型的字面量”,其实它的数据结构里面包含了一个CONSTANT_Utf8的指针,所以CONSTANT_Utf8才是真正存放字符串的数据结构。

运行时常量池

  运行时常量池是方法区的一部分,是全局共享的。我们知道,jvm在执行某个类的时候,必须经过加载、连接(验证,准备,解析)、初始化,在第一步的加载阶段,虚拟机需要完成下面3件事情:

  • 通过一个类的“全限定名”来获取此类的二进制字节流
  • 将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构
  • 在内存中生成一个类代表这类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口

  上面第二条中提到的静态存储结构就是包含了class文件中的静态常量池,所以静态常量池中的数据最终会进入到运行时常量池中,但是运行时常量池的数据还会来自运行时产生的。

  运行时常量池的作用是存储class文件常量池中的符号信息。运行时常量池 中保存着一些 class 文件中描述的符号引用,同时在类加载的“解析阶段”还会将这些符号引用所翻译出来的直接引用(直接指向实例对象的指针)存储在 运行时常量池 中。

  运行时常量池相对于 class 常量池一大特征就是其具有动态性,Java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池中的内容并不全部来自 class 常量池,class 常量池并非运行时常量池的唯一数据输入口;在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的较多的是String.intern()。

   ps:方法区位于永久代中,在JDK8版本,永久代被移除,新增了一个元空间,运行时常量池也被移动到了元空间中。元空间的作用与方法去类似,其与永久代最大区别在于前者不使用虚拟机内存,而是使用本地内存。

字符串常量池

  字符串常量池是JVM维护的一个字符串实例的引用的HashTable,在JDK7以前,字符串常量池位于永久代中,JDK7将字符串常量池移到了堆中,以下我们详细介绍JDK6,我们在最后介绍JDK7与JDK6的差别。在上面的运行时数据区看不到字符串常量池,所以我们换个更详细的图。

   这是将JVM内存按照堆-非堆的维度划分,让我们放大非堆的结构。

 

  可以看到,在非堆中的永久代除了方法区之外还有一个Interned Strings,这就是字符串常量池,值得注意的是,字符串常量池不会存储字符串对象本身,存储的是字符串的引用

以下是openjdk源码中字符串常量池HashTableEntry结构的一部分:

/hotspot/src/share/vm/utilities/hashtable.hpp

class BasicHashtableEntry : public CHeapObj {
  friend class VMStructs;
private:
  unsigned int         _hash;           // 32-bit hash for item
  BasicHashtableEntry* _next;
 } 

  其中_hash就是字符串的地址,字符串对象本身是存储在永久代中。

  在上面说到,JVM在加载一个类时会将该类的静态常量池的数据加载到运行时常量池,其实这是创建了CONSTANT_UTF8,并没有创建CONSTANT_STRING,只有当该字符串被引用到时,才会被创建。

在类加载过程中,有一个叫做解析(resolve)的步骤,在JVM规范中明确指出,这个阶段可以是lazy的,CONSTANT_STRING就是lazy resolve的。

  ps:CONSTANT_UTF8和CONSTANT_STRING是JVM使用的对象不是JAVA对象,JAVA程序只认识java.lang.String。

String进入到字符串常量池的过程

 1 public class TestClass {
 2 
 3     public static void main(String[] args) {
 4         String s = "123";
 5         String s1 = new String("1");
 6         String s2 = s1 + new String("23");
 7         String s3 = s2.intern();
 8         final String s4 = "1";
 9         String s5 = s4 + "23";
10         System.out.println(s==s3);
11        System.out.println(s==s5);
12     }
13

  当这个类编译完成后,在静态常量池中会创建一个保存有字符串"123"的CONSTANT_UTF8_INFO和一个保存有前者的index的CONSTANT_STRING_INFO。

  当程序启动时,JVM会加载该类,将静态常量池中的CONSTANT_UTF8_INFO加载到运行时常量池。

  当执行到第三行代码时,发现字符串"123"被引用,实例化CONSTANT_STRING_INFO,并且会在永久代创建字符串对象然后将字符串的引用保存到字符串常量池中。

  当执行到第五行代码时,会在堆中创建三个String对象,这是因为对于String的+运算符,如果参与运算的对象含有变量则会变成使用StringBuilder.appen来构建字符串,如果参与运算的对象都是常量的话,则在编译期间会直接将常量合并为一个字符串。

  当执行到第六行代码时,intern()方法会先判断字符串常量池中是否有字符串"123",如果有则返回引用,没有则在永久代创建该字符串,并且会在字符串常量池中保存该字符串的引用,然后返回引用。

  当执行到第八行代码时,JVM在字符串常量池中找到了字符串"123",返回其引用。

  所以最终结果是true true,同理我们可以分析文章开头那一段代码。

  综上,对于字面量来说,CONSTANT_UTF8是存储字符串内容的数据结构,CONSTANT_STRING则持有前者的引用,当字面量被引用时CONSTANT_STRING才会被创建,并且同时字符串常量池会保存字符串的引用。对于intern()方法来说,如果常量池没有该字符串,则会在永久代创建该字符串并返回引用,否则,直接返回常量池中字符串的引用。

JDK7与JDK6字符串常量池的区别

在JDK7以前,字符串常量池位于永久代中,JDK7将字符串常量池移到了堆中,所以在永久代创建字符串变成了在堆中创建。其行为也有一些不同之处。让我们在JDK7下来分析文章开头的代码中:

 1 String s = new String("1");
 2 s.intern();
 3 String s2 = "1";
 4 System.out.println(s == s2);
 5 
 6  
 7 String s3 = new String("1") + new String("1");
 8 s3.intern();
 9 String s4 = "11";
10 System.out.println(s3 == s4); 

  第一段的第一行代码在堆中创建了两个字符串对象,第一个是String构造函数中的字面量"1",该字符串的引用会被存到字符串常量池中,第二个是new的一个String对象,它的内容与字面量"1"相同。然后将s保存到字符串常量中,由于常量池中已存在字符串"1",所以intern()没有更新常量池。s2是字面量"1"也就是常量池中的对象,所以最终结果s!=s2。

  第二段代码的第一行在堆中创建了一个内容为"11"的字符串。然后将该字符串保存到字符串常量池中,由于字符串常量池中没有"11"的字符串,所以字符串常量池会保存该字符串的引用。然后s4是字面量"11",由于字符串常量池中存在"11"字符串,所以返回该字符串的引用,所以最终s3==s4。

最终状态图如下所示:

 

在JDK6下,最终结果如下图所示:

参考:

原文地址:https://www.cnblogs.com/ouhaitao/p/12132113.html