【jvm jdk】Class文件中的常量池详解(上)

NO1.常量池在class文件的什么位置?

在class文件中的魔数副版本号主版本之后,紧接着就是常量池的数据区域了,如下图用红线包括的位置:

NO2.常量池的里面是怎么组织的?

常量池的组织很简单,前端的两个字节占有的位置叫做常量池计数器(constant_pool_count),它记录着常量池的组成元素 常量池项(cp_info) 的个数。紧接着会排列着constant_pool_count-1个常量池项(cp_info)。如下图所示:
在这里插入图片描述

常量池计数器占两个字节

NO3.常量池项 (cp_info) 的结构是什么?

每个常量池项(cp_info) 都会对应记录着class文件中的某中类型的字面量。让我们先来了解一下常量池项(cp_info)的结构吧:

在这里插入图片描述
JVM虚拟机规定了不同的tag值和不同类型的字面量对应关系如下,截至JDK13,常量表中分别有17种不同类型的常量:
在这里插入图片描述
所以根据cp_info中的tag 不同的值,可以将cp_info 更细化为以下结构体:

CONSTANT_Utf8_info,CONSTANT_Integer_info,CONSTANT_Float_info,CONSTANT_Long_info,
CONSTANT_Double_info,CONSTANT_Class_info,CONSTANT_String_info,CONSTANT_Fieldref_info,
CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info,CONSTANT_NameAndType_info,CONSTANT_MethodHandle_info,
CONSTANT_MethodType_info,CONSTANT_InvokeDynamic_info

在这里插入图片描述

常量池项可能是描述一个具体的值,也可能是一个引用,这个引用指向另一个常量池项

现在让我们看一下细化了的常量池的结构会是类似下图所示的样子:
在这里插入图片描述

NO4.常量池能够表示那些信息?

在这里插入图片描述

除了基本类型,其他最终都指向UTF8,即所有的符号引用最终都会指向UTF8

NO5. int和float数据类型的常量在常量池中是怎样表示和存储的?

(CONSTANT_Integer_info, CONSTANT_Float_info)

Java语言规范规定了 int类型和Float 类型的数据类型占用 4 个字节的空间。那么存在于class字节码文件中的该类型的常量是如何存储的呢?相应地,在常量池中,将 intFloat类型的常量分别使用CONSTANT_Integer_infoConstant_float_info表示,他们的结构如下所示:

在这里插入图片描述
举例:建下面的类 IntAndFloatTest.java,在这个类中,我们声明了五个变量,但是取值就两种int类型的10 和Float类型的11f。

public class IntAndFloatTest {
    
    
	
	private final int a = 10;
	private final int b = 10;
	private float c = 11f;
	private float d = 11f;
	private float e = 11f;
	
}

然后用编译器编译成IntAndFloatTest.class字节码文件,我们通过javap -v IntAndFloatTest 指令来看一下其常量池中的信息,如下图所示:

可以看到虽然我们在代码中写了两次10三次11f,但是常量池中,就只有一个常量10 和一个常量11f

Constant pool:
   #1 = Class              #2             // IntAndFloatTest
   #2 = Utf8               IntAndFloatTest
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               ConstantValue
  `#8 = Integer            10`
   #9 = Utf8               b
  #10 = Utf8               c
  #11 = Utf8               F
  #12 = Utf8               d
  #13 = Utf8               e
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Methodref          #3.#18         // java/lang/Object."<init>":()V
  #18 = NameAndType        #14:#15        // "<init>":()V
  #19 = Fieldref           #1.#20         // IntAndFloatTest.a:I
  #20 = NameAndType        #5:#6          // a:I
  #21 = Fieldref           #1.#22         // IntAndFloatTest.b:I
  #22 = NameAndType        #9:#6          // b:I
 `#23 = Float              11.0f`
  #24 = Fieldref           #1.#25         // IntAndFloatTest.c:F
  #25 = NameAndType        #10:#11        // c:F
  #26 = Fieldref           #1.#27         // IntAndFloatTest.d:F
  #27 = NameAndType        #12:#11        // d:F
  #28 = Fieldref           #1.#29         // IntAndFloatTest.e:F
  #29 = NameAndType        #13:#11        // e:F
  #30 = Utf8               LineNumberTable
  #31 = Utf8               LocalVariableTable
  #32 = Utf8               this
  #33 = Utf8               LIntAndFloatTest;
  #34 = Utf8               SourceFile
  #35 = Utf8               IntAndFloatTest.java

从结果上可以看到常量池第#8 个常量池项(cp_info) 就是CONSTANT_Integer_info,值为10;第#23个常量池项(cp_info) 就是CONSTANT_Float_info,值为11f。(常量池中其他的东西先别纠结啦,我们会面会一一讲解的哦)。

代码中所有用到 int 类型 10 的地方,会使用指向常量池的指针值#8 定位到第#8 个常量池项(cp_info),即值为 10的结构体 CONSTANT_Integer_info,而用到float类型的11f时,也会指向常量池的指针值#23来定位到第#23个常量池项(cp_info) 即值为11f的结构体CONSTANT_Float_info。如下图所示:

在这里插入图片描述

这里有个疑问,之前介绍常量池字面量时,涉及文本字符串,被声明为final的常量值,可是11f不是final修饰的啊?

NO6. long和 double数据类型的常量在常量池中是怎样表示和存储的?

(CONSTANT_Long_info、CONSTANT_Double_info )

和NO5类似,这里就不再展示了,想了解的话可以参考原文

NO7. String类型的字符串常量在常量池中是怎样表示和存储的?

(CONSTANT_String_info、CONSTANT_Utf8_info)

对于字符串而言,JVM会将字符串类型的字面量以UTF-8 编码格式存储到在class字节码文件中。这么说可能有点摸不着北,我们先从直观的Java源码中中出现的用双引号"" 括起来的字符串来看,在编译器编译的时候,都会将这些字符串转换成CONSTANT_String_info结构体,然后放置于常量池中。其结构如下所示:
在这里插入图片描述
如上图所示的结构体,CONSTANT_String_info结构体中的string_index的值指向了CONSTANT_Utf8_info结构体,而字符串的utf-8编码数据就在这个结构体之中。如下图所示:
在这里插入图片描述

顺便提一下,由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2两个字节类型能表达的最大值65535所以Java程序中如果定义了超过64KB英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。

请看一例,定义一个简单的StringTest.java类,然后在这个类里加一个"JVM原理" 字符串,然后,我们来看看它在class文件中是怎样组织的。


public class StringTest {
    
    
    private String s1 = "JVM原理";
    private String s2 = "JVM原理";
    private String s3 = "JVM原理";
    private String s4 = "JVM原理";
}

将Java源码编译成StringTest.class文件后,在此文件的目录下执行 javap -v StringTest 命令,会看到如下的常量池信息的轮廓:

Constant pool:
   #1 = Class              #2             // StringTest
   #2 = Utf8               StringTest
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               s1
   #6 = Utf8               Ljava/lang/String;
   #7 = Utf8               s2
   #8 = Utf8               s3
   #9 = Utf8               s4
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Methodref          #3.#14         // java/lang/Object."<init>":()V
  #14 = NameAndType        #10:#11        // "<init>":()V
 `#15 = String             #16            // JVM原理 `  CONSTANT_String_info结构
 `#16 = Utf8               JVM原理`  CONSTANT_Utf8_info结构
  #17 = Fieldref           #1.#18         // StringTest.s1:Ljava/lang/String;
  #18 = NameAndType        #5:#6          // s1:Ljava/lang/String;
  #19 = Fieldref           #1.#20         // StringTest.s2:Ljava/lang/String;
  #20 = NameAndType        #7:#6          // s2:Ljava/lang/String;
  #21 = Fieldref           #1.#22         // StringTest.s3:Ljava/lang/String;
  #22 = NameAndType        #8:#6          // s3:Ljava/lang/String;
  #23 = Fieldref           #1.#24         // StringTest.s4:Ljava/lang/String;
  #24 = NameAndType        #9:#6          // s4:Ljava/lang/String;
  #25 = Utf8               LineNumberTable
  #26 = Utf8               LocalVariableTable
  #27 = Utf8               this
  #28 = Utf8               LStringTest;
  #29 = Utf8               SourceFile
  #30 = Utf8               StringTest.java

PS :使用javap -v 指令能看到易于我们阅读的信息,查看真正的字节码文件可以使用HEXWin、NOTEPAD++、UtraEdit 等工具。

在面的图中,我们可以看到CONSTANT_String_info结构体位于常量池的第#15个索引位置。而存放"Java虚拟机原理" 字符串的 UTF-8编码格式的字节数组被放到CONSTANT_Utf8_info结构体中,该结构体位于常量池的第#16个索引位置。上面的图只是看了个轮廓,让我们再深入地看一下它们的组织吧。请看下图:

在这里插入图片描述

由上图可见:“JVM原理”的UTF-8编码的数组是:4A564D E5 8E 9FE7 90 86,并且存入了CONSTANT_Utf8_info结构体中,length为9个字节。

NO8. 类文件中定义的类名和类中使用到的类在常量池中是怎样被组织和存储的?

(CONSTANT_Class_info)

JVM会将某个Java 类中所有使用到了的类的完全限定名 以二进制形式的完全限定名 封装成CONSTANT_Class_info结构体中,然后将其放置到常量池里。CONSTANT_Class_info 的tag值为 7 。其结构如下:
在这里插入图片描述
Tips:类的完全限定名二进制形式的完全限定名

在某个Java源码中,我们会使用很多个类,比如我们定义了一个 ClassTest的类,并把它放到com.louis.jvm 包下,则 ClassTest类的完全限定名com.louis.jvm.ClassTest,将JVM编译器将类编译成class文件后,此完全限定名在class文件中,是以二进制形式的完全限定名存储的,即它会把完全限定符的"."换成"/" ,即在class文件中存储的 ClassTest类的完全限定名称是"com/louis/jvm/ClassTest"。因为这种形式的完全限定名是放在了class二进制形式的字节码文件中,所以就称之为 二进制形式的完全限定名

举例,我们定义一个很简单的ClassTest类,来看一下常量池是怎么对类的完全限定名进行存储的。

import java.util.Date;

public class ClassTest {
    
    
    private Date date = new Date();
}

将Java源码编译成ClassTest.class文件后,在此文件的目录下执行 javap -v ClassTest 命令,会看到如下的常量池信息的轮廓:

在这里插入图片描述
如上图所示,在ClassTest.class文件的常量池中,共有 3CONSTANT_Class_info结构体,分别表示ClassTest 中用到的Class信息。 我们就看其中一个表示com/jvm.ClassTestCONSTANT_Class_info 结构体。它在常量池中的位置是#1,它的name_index值为#2,它指向了常量池的第2 个常量池项,如下所示:
在这里插入图片描述

引入某个类的声明,但是没有编译进常量池

对于某个类而言,其class文件中至少要有两个CONSTANT_Class_info常量池项,用来表示自己的类信息和其父类信息。(除了java.lang.Object类除外,其他的任何类都会默认继承自java.lang.Object)如果类声明实现了某些接口,那么接口的信息也会生成对应的CONSTANT_Class_info常量池项。

除此之外,如果在类中使用到了其他的类,只有真正使用到了相应的类,JDK编译器才会将类的信息组成CONSTANT_Class_info常量池项放置到常量池中,如果仅是声明引入类,则不会实际编译进常量池。如下面的代码:


import java.util.Date;   //引入一个类
 
public  class Other{
    
    
	private Date date;  //声明一个成员变量,但是没有被用到
	
	public Other()
	{
    
    
		Date da;   //局部变量
	}
}

执行javap -v Other,只有2个类常量,分别是类自身和Object类:

Constant pool:
  `#1 = Class `            #2             // Other
   #2 = Utf8               Other
  `#3 = Class `             #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               date
   #6 = Utf8               Ljava/util/Date;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Methodref          #3.#11         // java/lang/Object."<init>":()V
  #11 = NameAndType        #7:#8          // "<init>":()V
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LOther;
  #16 = Utf8               SourceFile
  #17 = Utf8               Other.java

上述的Other的类,在JDK将其编译成class文件时,常量池中并没有java.util.Date对应的CONSTANT_Class_info常量池项,为什么呢?
在Other类中虽然定义了Date类型的两个变量date、da,但是JDK编译的时候,认为你只是声明了“Ljava/util/Date”类型的变量,并没有实际使用到Ljava/util/Date类。将类信息放置到常量池中的目的,是为了在后续的代码中有可能会反复用到它。很显然,JDK在编译Other类的时候,会解析到Date类有没有用到,发现该类在代码中就没有用到过,所以就认为没有必要将它的信息放置到常量池中了。

将上述的Other类改写一下,仅使用new Date(),如下图所示:

import java.util.Date;
 
public  class Other{
    
    
	public Other()
	{
    
    
		new Date();
	}
}

这时候使用javap -v Other ,可以查看到常量池中有表示java/util/Date的常量池项:
在这里插入图片描述
总结:

  • 对于某个类或接口而言,其自身父类和继承或实现的接口的信息会被直接组装成CONSTANT_Class_info常量池项放置到常量池中;

  • 类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有对应的CONSTANT_Class_info常量池项;

  • 类中或接口中仅仅定义某种类型的变量,JDK只会将变量的类型描述信息以UTF-8字符串组成CONSTANT_Utf8_info常量池项放置到常量池中,上面在类中的private Date date;JDK编译器只会将表示date的数据类型的“Ljava/util/Date”字符串放置到常量池中。

    有关Ljava/util/Date的描述,可以参考 Class文件中的常量池详解(下)《字段的数据类型》章节

参考 《Java虚拟机原理图解》 1.2.2、Class文件中的常量池详解(上)

猜你喜欢

转载自blog.csdn.net/m0_45406092/article/details/108872575