JVM常量池和运行时常量池

Posted wen-pan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM常量池和运行时常量池相关的知识,希望对你有一定的参考价值。

一、类的二进制字节码包含哪些信息

要理解常量池是什么,先看看类的二进制字节码包含哪些信息!!!

  • 常量池
  • 类的基本信息(比如:类的访问权限、类的名称、实现了哪些接口)
  • 类的方法定义(包含了虚拟机指令,也就是把我们代码编译为了虚拟机指令 )

二、通过反编译字节码验证

1、测试代码

将下面的测试代码使用javac 编译为 *.class文件

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

2、javap反编译*.class字节码

先将示例代码编译为 *.class 文件,然后将class文件反编译为JVM指令码。然后观察 *.class字节码中到底包含了哪些部分。

// ===========================================类的描述信息===============================================
Classfile /xx/xx/xx/xx/HelloWorld.class
  Last modified 2021-10-12; size 569 bytes
  MD5 checksum 7f4f0fe4b6e6d04ddaf30401a7b04f07
  Compiled from "HelloWorld.java"
public class org.memory.jvm.t5.HelloWorld
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
    
// ===========================================常量池===============================================
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // org/memory/jvm/t5/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/memory/jvm/t5/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               org/memory/jvm/t5/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
                            
// =======================================虚拟机中执行编译的方法===========================================
{
  public org.memory.jvm.t5.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/memory/jvm/t5/HelloWorld;
	
  // main方法JVM指令码
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    // main方法访问修饰符描述
    flags: ACC_PUBLIC, ACC_STATIC
    // main方法中的代码执行部分
    // ===============================解释器读取下面的JVM指令解释并执行===================================             
    Code:
      stack=2, locals=1, args_size=1
         // 从常量池中符号地址为 #2 的地方,先获取静态变量System.out
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 从常量池中符号地址为 #3 的地方加载常量 hello world
         3: ldc           #3                  // String hello world
         // 从常量池中符号地址为 #3 的地方获取要执行的方法描述,并执行方法输出hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         // main方法返回
         8: return
    // ==================================解释器读取上面的JVM指令解释并执行================================
      // 行号映射表
      LineNumberTable:
        line 9: 0
        line 10: 8
      // 局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

三、什么是常量池以及常量池的作用

1、什么是常量池

从上面的反编译字节码中可以看到,Class的常量池其实就是一张记录着该类的一些常量、方法描述、类描述、变量描述信息的表。

2、常量池中有什么内容

常量池中主要存放两类数据,一是字面量、二是符号引用

字面量:

  • 比如String类型的字符串值或者定义为final类型的常量的值。

符号引用:

  • 类或接口的全限定名(包括他的父类和所实现的接口)
  • 变量或方法的名称
  • 变量或方法的描述信息
  • this

可参考:https://blog.csdn.net/Hellowenpan/article/details/101389330

3、常量池的作用

在解释器解释执行每条JVM指令码的时候,根据这些指令码的符号地址去常量池中找到对应的描述。然后解释器就知道该执行哪个类的那个方法、方法的参数是什么等。

拿上面反编译的字节码指令来说明:

  1. 当解释器解释执行main方法的时候,读取到下面的11行JVM指令码0: getstatic #2
  2. getstatic指令表示获取一个静态变量,#2表示该静态变量的符号地址,解释器通过#2符号地址去常量池中查找#2对应的静态变量
  3. 然后解释器继续向下运行,执行第13行的3: ldc #3指令,该指令的含义是:从常量池中加载符号地址为 #3 的常量
  4. 然后解释器继续向下运行,执行第15行的5: invokevirtual #4指令,该指令的含义是:执行方法,那么要执行哪个方法呢?执行常量池中符号地址为 #4 的方法。
  // main方法JVM指令码
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    // main方法访问修饰符描述
    flags: ACC_PUBLIC, ACC_STATIC
    // main方法中的代码执行部分
    // ===============================解释器读取下面的JVM指令解释并执行===================================             
    Code:
      stack=2, locals=1, args_size=1
         // 从常量池中符号地址为 #2 的地方,先获取静态变量System.out
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 从常量池中符号地址为 #3 的地方加载常量 hello world
         3: ldc           #3                  // String hello world
         // 从常量池中符号地址为 #3 的地方获取要执行的方法描述,并执行方法输出hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         // main方法返回
         8: return
    // ==================================解释器读取上面的JVM指令解释并执行================================

四、运行时常量池

1、什么是运行时常量池

上面我们分析了常量池其实就是一张对照表,常量池是 *.class 文件中的。当类的字节码被加载到内存中后,他的常量池信息就会集中放入到一块内存,这块内存就称为运行时常量池,并且把里面的符号地址为真实地址

2、符号地址变为真实地址怎么理解

①、符号地址

从上面的反编译后的JVM字节码指令可以看到有这么一条指令0: getstatic #2,解释器解释执行JVM指令的时候,通过指令中的 #x去常量池中获取需要的值。这里的#2其实就是符号地址,标识这某个变量在常量池中的某个位置。

②、真实地址

在程序运行期,当*.Class文件被加载到内存以后,常量池中的这些描述信息就会被放到内存中,其中的 #x会被转化为内存中的地址(真实地址)。

③、简单总结

符号地址变为真实地址其实就是,在*.class文件被加载到内存以后,将*.class文件中常量池中的#x符号地址,转化为内存中的地址。

以上是关于JVM常量池和运行时常量池的主要内容,如果未能解决你的问题,请参考以下文章

Java运行时常量池是啥?

Jave运行时常量池是啥意思?

JVM详解之:运行时常量池

JVM09_方法区

常量池

JVM 常量池运行时常量池字符串常量池