文章目录
相关文章:
Class文件中的常量池详解(上)
Class文件中的常量池详解(下)
JVM类加载器机制与类加载过程(运行main方法的原理)
NO1.常量池在class文件的什么位置?
在class文件中的魔数
、副版本号
、主版本
之后,紧接着就是常量池的数据区域了,如下图用红线包括的位置:
NO2.常量池的里面是怎么组织的?
常量池的组织很简单,前端的两个字节占有的位置叫做常量池计数器
(constant_pool_count
),它记录着常量池的组成元素 常量池项(cp_info
) 的个数。紧接着会排列着constant_pool_count-1
个常量池项(cp_info)。如下图所示:
常量池计数器
占两个字节
NO3.常量池项 (cp_info) 的结构是什么?
每个常量池项(cp_info) 都会对应记录着class文件中的某中类型的字面量。让我们先来了解一下常量池项(cp_info)的结构吧:
从上图中,可以看到,一个cp_info结构分为2个部分,tag和Info[],其中tag表示当前结构对应的实际类型,可以参见下图,例如为1时,表示CONSTANT_Utf8_info结构,当为2时,表示CONSTANT_Integer_info。另外的字段,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
其中原子类型(基础类型)结构包括CONSTANT_Utf8_info,CONSTANT_Integer_info,CONSTANT_Float_info,CONSTANT_Long_info,CONSTANT_Double_info,而其余类型可以最终拆解为上述原子类型结构:
常量池项可能是描述一个具体的值,也可能是一个引用,这个引用指向另一个常量池项
现在让我们看一下细化了的常量池的结构会是类似下图所示的样子:
NO4.常量池能够表示那些信息?
除了基本类型,其他
最终
都指向UTF8
,即所有的符号引用最终都会指向UTF8
NO5. int和float数据类型的常量在常量池中是怎样表示和存储的?
(CONSTANT_Integer_info, CONSTANT_Float_info)
Java语言规范规定了 int类型和Float 类型的数据类型占用 4 个字节的空间。那么存在于class字节码文件中的该类型的常量是如何存储的呢?相应地,在常量池中,将 int
和Float
类型的常量分别使用CONSTANT_Integer_info
和 Constant_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
文件的常量池中,共有 3
个CONSTANT_Class_info
结构体,分别表示ClassTest 中用到的Class信息。 我们就看其中一个表示com/jvm.ClassTest
的CONSTANT_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文件中的常量池详解(下)《字段的数据类型》章节