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

2019-10-23  本文已影响0人  BigX

这篇文章主要是做一个总结,将查找到相关的资料自己做一个整理,最后会列出查找过的相关资料,感兴趣的可以去翻一翻。

常量池

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

那么字符串常量池中引用的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不包含该对象字面量和引用时,将该字符串对象引用存入字符串常量中 ,同时返回该地址。这样做的目的是为了提升性能,降低开销,后续如果定义相同字面量的字符串即可返回该引用(内存地址),不必再在堆上创建字符串实例。

上一篇 下一篇

猜你喜欢

热点阅读