JVM笔记:Java虚拟机的常量池

常量池

  • class文件常量池(class constant pool)

    常量池能够理解为Class文件之中的资源仓库,它是Class文件结构中与其余项目关联最多的数据类型,包含了类也是占用Class文件中第一个出现的表类型数据项目。java

    常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包含了下面三类常量:bash

    • 类和接口的全限定名(Full Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符

    类和接口的全限定名,例如:com/example/demo/Demo.classapp

    字段的名称和描述符,例如:Field a:[Ljava/lang/Stringpost

    方法的名称和描述符,例如:Method java/lang/String."<init>":(Ljava/lang/String;)V性能

    后两个是字节码指令,不懂得能够查阅下相关资料(TODO) + 能够经过查看字节码的形式来查看Class的常量池的内容,由于是在编译时产生的,也能够称为静态常量池优化

public class Main {
   private int a=1;
   private int b=1;
   private Aload c=new Aload();
   private String [] d =new String[10];
   public static void main(String[] args) {

   }
}
字节码:
public class com.verzqli.snake.Main
 minor version: 0
 major version: 51
 flags: ACC_PUBLIC, ACC_SUPER
Constant pool: //这里就是class文件的常量池
  #1 = Methodref #10.#30 // java/lang/Object."<init>":()V
  #2 = Fieldref #9.#31 // com/verzqli/snake/Main.a:I
  #3 = Fieldref #9.#32 // com/verzqli/snake/Main.b:I
  #4 = Class #33 // com/verzqli/snake/Aload
  #5 = Methodref #4.#30 // com/verzqli/snake/Aload."<init>":()V
  #6 = Fieldref #9.#34 // com/verzqli/snake/Main.c:Lcom/verzqli/snake/Aload;
  #7 = Class #35 // java/lang/String
  #8 = Fieldref #9.#36 // com/verzqli/snake/Main.d:[Ljava/lang/String;
  #9 = Class #37 // com/verzqli/snake/Main
 #10 = Class #38 // java/lang/Object
 #11 = Utf8 a
 #12 = Utf8 I
 #13 = Utf8 b
 #14 = Utf8 c
 #15 = Utf8 Lcom/verzqli/snake/Aload;
 #16 = Utf8 d
 #17 = Utf8 [Ljava/lang/String;
 #18 = Utf8 <init>
 #19 = Utf8 ()V
 #20 = Utf8 Code
 #21 = Utf8 LineNumberTable
 #22 = Utf8 LocalVariableTable
 #23 = Utf8 this
 #24 = Utf8 Lcom/verzqli/snake/Main;
 #25 = Utf8 main
 #26 = Utf8 ([Ljava/lang/String;)V
 #27 = Utf8 args
 #28 = Utf8 SourceFile
 #29 = Utf8 Main.java
 #30 = NameAndType #18:#19 // "<init>":()V
 #31 = NameAndType #11:#12 // a:I
 #32 = NameAndType #13:#12 // b:I
 #33 = Utf8 com/verzqli/snake/Aload
 #34 = NameAndType #14:#15 // c:Lcom/verzqli/snake/Aload;
 #35 = Utf8 java/lang/String
 #36 = NameAndType #16:#17 // d:[Ljava/lang/String;
 #37 = Utf8 com/verzqli/snake/Main
 #38 = Utf8 java/lang/Object
复制代码
  • 运行时常量池

    当java文件被编译成class文件以后,就会生成上面的常量池,在Class文件中描述的各类信息,最终都须要加载到虚拟机中以后才能运行和使用。 类从被加载到虚拟机内存中开始,到卸载出内存位置,他的生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initalization)、使用(Using)和卸载(Unloading),其中验证、准备、解析三个部分统称Wie链接(Linking)。ui

    而当类加载到内存中后,JVM就会将Class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每一个类都有一个。在解析过程当中须要将常量池中全部的符号引用(classes、interfaces、fields、methods referenced in the constant pool)转为直接引用(获得类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。直接引用能够是内存中,直接指向目标的指、相对偏移量,或是一个能间接定位到目标的句柄,解析的这个阶段其实就是将符号引用转换为能够直接定位对象等在内存中的位置的直接引用。this

    运行时常量池位于JVM规范的方法区中,在Java8之前,位于永生代;Java8以后位于元空间。spa

  • 全局字符串常量池(string pool / string literal pool)

    全局字符串池里的内容是在类加载完成,通过验证,准备阶段以后在堆中生成字符串对象实例,而后将该字符串对象实例的引用值存到string pool中。在HotSpot中具体实现string pool这一功能的是StringTable类,它是一个哈希表,里面存的是key(字面量“abc”, 即驻留字符串)-value(字符串"abc"实例对象在堆中的引用)键值对,StringTable自己存在本地内存(native memory)中。.net

    StringTable在每一个HotSpot VM的实例只有一份,被全部的类共享(享元模式)。在Java7的时候将字符串常量池移到了堆里,同时里面也不在存放对象(Java7之前被intern的String对象存放于永生代,因此很容易形成OOM),而是存放堆上String实例对象的引用。

    那么字符串常量池中引用的String对象是在何时建立的呢?在JVM规范里明确指定resolve阶段能够是lazy的,即在须要进行该符号引用的解析时才去解析它,这样的话,可能该类都已经初始化完成了,若是其余的类连接到该类中的符号引用,须要进行解析,这个时候才会去解析。

    这时候就须要ldc这个字节码指令,其做用是将int、float或String型常量值从常量池中推送至栈顶,以下面这个例子。

public class Main {
    public static void main(String[] args) {
      String a="B";
    }
}
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2 // String B
         2: astore_1
         3: return
      LineNumberTable:
        line 14: 0
        line 15: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
            3       1     1     a   Ljava/lang/String;
}
复制代码

在main方法的字节码中使用ldc将字符串“B”推到栈顶,而后赋值给局部变量a,最后退出。

根据上面说的,在类加载阶段,这个 resolve 阶段( constant pool resolution )是lazy的。换句话说并无真正的对象,字符串常量池里天然也没有,那么ldc指令还怎么把人推送至栈顶?或者换一个角度想,既然resolve 阶段是lazy的,那总有一个时候它要真正的执行吧,是何时?执行ldc指令就是触发这个lazy resolution动做的条件

ldc字节码在这里的执行语义是:到当前类的运行时常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找该index对应的项,若是该项还没有resolve则resolve之,并返回resolve后的内容。

在遇到String类型常量时,resolve的过程若是发现StringTable已经有了内容匹配的java.lang.String的引用,则直接返回这个引用,反之,若是StringTable里还没有有内容匹配的String实例的引用,则会在Java堆里建立一个对应内容的String对象,而后在StringTable记录下这个引用,并返回这个引用出去。可见,ldc指令是否须要建立新的String实例,全看在第一次执行这一条ldc指令时,StringTable是否已经记录了一个对应内容的String的引用。

public class Main {
    String a="b";
    public static void main(String[] args) {
    }
}

public com.verzqli.snake.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2 // String b
         7: putfield      #3 // Field a:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/verzqli/snake/Main;
复制代码

上面例子执行完main方法后,“b”就不会进入字符串常量池。由于String a = "b"是Main类的成员变量,成员变量只有在执行到构造方法的时候才会初始化。

往细讲,只有执行了ldc指令的字符串才会进入字符串常量池

至于ldc指令的工做原理能够看这篇文章

String.intern()

当一个字符串对象调用这个intern方法时,若是该字符串常量池中不包含该对象引用,也即StringTable不包含该对象字面量和引用时,将该字符串对象引用存入字符串常量中 ,同时返回该地址。这样作的目的是为了提高性能,下降开销,后续若是定义相同字面量的字符串便可返回该引用(内存地址),没必要再在堆上建立字符串实例。

  • 实例(如下实例环境为JDK7之后)

    String a="c";
          String b = new String("c");
          System.out.println("a==b.intern()="+(a==b.intern()));
          System.out.println("b==b.intern()="+(b==b.intern()));
          
          结果:
          a==b.intern()=true
          b==b.intern()=false
    复制代码

    类加载阶段,什么都没干。

    而后运行main方法,建立“c”对象 ,假设其地址为0xeee,将其加入字符串常量池。随后在堆上建立了String对象b,假设其地址为0xfff

    这里b.intern()检测到了字符串常量池中包含“c”这个字符串引用,因此其返回的是0xeee,而b指向的依旧是0xfff,因此第一个为true,第二个为false

    String a = new String("hellow") + new String("orld");
         String b = new String("hello") + new String("world");
         System.out.println("a==a.intern()="+(a==a.intern()));
         System.out.println("a==b.intern()="+(a==b.intern()));
         System.out.println("b==b.intern()="+(b==b.intern()));
    
       结果:
      a==b.intern()=true
      a==b.intern()=true
      b==b.intern()=false
    复制代码

    类加载阶段,什么都没干。

    而后运行main方法,建立“hellow”,"orld"对象,并放入字符串常量池。而后会建立一个"helloworld"对象,没有放入字符串常量池,a指向这个"helloworld"对象(0xeee)。

    接着建立“hello”,"world"对象,一样也建立一个"helloworld"对象,也没有放入字符串常量池,b指向这个"helloworld"对象地址(0xfff)。

    这时候第一个判断,字符串常量池没有“helloworld”这个字符串对象引用,因此将a的引用(0xeee)放入字符串常量池,也就是说池子中的引用和a的引用(0xeee)是同样的,因此a==a.intern()

    b.intern()时由于上一部字符串常量池中已经有了这个“helloworld”的引用,因此他返回回去的引用(0xeee)就是a的引用,因此a==b.intern()

    从上面能够清楚的知道b.intern()返回的是0xfff,而b引用地址为0xfff,因此b!=b.intern()

    //        String a1="helloworld";
        String a = new String("hello")+new String("world");
        System.out.println("a==a=" + (a == a.intern()));
    复制代码

    这里的结果若是a1没有被注释则为false,注释了则为true,原理同上,能够本身脑补一下。

  • JVM对字符串的优化

    String a = "hello";
        String b = a+"world";
        String c = "helloworld";
        String d = "hello"+"world";
        System.out.println(b==c); false
        System.out.println(d==c); true
        System.out.println(b==d); false
        
            Code:
      stack=3, locals=5, args_size=1
         0: ldc           #4 // String hello //ldc指令建立字符串对象“hello”
         2: astore_1                          // 将a从放入局部变量表(第一个局部变量,第0个是this)
         3: new           #5 // class java/lang/StringBuilder //建立StringBuilder对象
         6: dup                               // 复制栈顶数据(建立StringBuilder对象)压入栈中
         7: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 
        10: aload_1                           // 从局部变量中载入a到栈中
        11: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //能够看出字符串相加在字节码里就是StringBuilder的append
        14: ldc           #8 // String world /ldc指令建立字符串对象“world”
        16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//继续append
        19: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; //相加完毕,隐形的调用toString生成String对象返回
        22: astore_2                          // 将b放入局部变量表(第二个局部变量)   
        23: ldc           #10 // String helloworld //ldc指令建立字符串对象“helloworld”
        25: astore_3                          // 将c放入局部变量表(第三个局部变量) 
        26: ldc           #10 // String helloworld //这里字符串常量池中已经包含了helloworld,就不会再建立,直接引用,并且这个helloworld是"hello"+"world"拼接的,这就是JVM对字符串的优化
        28: astore        4                   // 将d放入局部变量表(第四个局部变量) 
        30: getstatic     #11 // Field java/lang/System.out:Ljava/io/PrintStream; //调用静态方法打印
        33: aload_2                           // 从局部变量表加载b入栈
        34: aload_3                           // 从局部变量表加载c入栈
        35: if_acmpne     42                  // 比较两个对象的引用类型 下面四行就是一个if else 语句,若是相等就直接doto打印结果,
        38: iconst_1                          // 得到两个引用是否相等的结果(true为1,false为0),将1入栈
        39: goto          43                  // 跳转到43行 直接打印出结果
        42: iconst_0                          // 两引用不相等,将0入栈 
        43: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
        46: getstatic     #11 // Field java/lang/System.out:Ljava/io/PrintStream;
        后续都是相同的意思,这里就不注释了。
        49: aload         4
        51: aload_3
        52: if_acmpne     59
        55: iconst_1
        56: goto          60
        59: iconst_0
        60: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
        63: getstatic     #11 // Field java/lang/System.out:Ljava/io/PrintStream;
        66: aload_2
        67: aload         4
        69: if_acmpne     76
        72: iconst_1
        73: goto          77
        76: iconst_0
        77: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
        80: return
    复制代码

    从上面的字节码能够看出字符串的相加实际上是new了一个StringBuilder来进行append,a和b不相等就是由于这已是两个不一样的对象了,引用也不相等。后续c和d相等是由于JVM对纯字符串想加作了调优,会在字节码中把他们直接相加后的值赋给局部变量,因此c和d指向的是同一个字符串。

    String a= "a";
        for (int i = 0; i < 3; i++) {
            a+="b";
        }
        
        Code:
      stack=2, locals=3, args_size=1
         0: ldc           #4 // String a
         2: astore_1
         3: iconst_0
         4: istore_2
         5: iload_2
         6: iconst_3
         7: if_icmpge     36
        10: new           #5 // class java/lang/StringBuilder
        13: dup
        14: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
        17: aload_1
        18: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: ldc           #2 // String b
        23: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        26: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: astore_1
        30: iinc          2, 1
        33: goto          5
        36: return
    复制代码

    对于for循环中的字符串相加(3到33行就是for循环的内容),JVM就没有优化了,每次相加都是从新建立了StringBuilder,开销就是一个StringBuilder的几何倍数那么大,于是在循环中使用StringBuilder的append来替代直接相加。

  • 总结

    除了平常的若是以为文章有错误,欢迎指出并交流。这里问一个问题,后续若是知道了再删除:字符串常量池和StringTable是一个东西吗,二者都是存的字符串引用,可是R大说过StringTable是存于本地内存(native memory),可是看过的文章都说的是字符串常量池位于java堆中,但愿有知道的大佬能够告知一下。

  • 引用:

    完全搞懂string常量池和intern

    JVM 常量池中存储的是对象仍是引用呢?

    Java String实例的建立和常量池的关系及intern方法

    Java 中new String("字面量") 中 "字面量" 是什么时候进入字符串常量池的?

相关文章
相关标签/搜索