自己的学习笔记,部分节选自《揭秘java虚拟机》
前言
对于一个class文件,内容有: 以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数
这里主要说class文件中的常量池: constant_pool_count 常量池计数器,constant_pool_count的值等于constant_pool表中的成员数加1。constant_pool 表的索引值只有在大于 0 且小于 constant_pool_count 时才会被认为是有效的 ,对于 long 和 double 类型有例外情况。
注意:虽然值为 0 的 constant_pool 索引是无效的,但其他用到常量池的数据结构可以使用索引 0 来表示“不引用任何一个常量池项”的意思。
constant_pool[ ] 常量池,constant_pool 是一种表结构,它包含 Class 文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。常量池中的每一项都具备相同的格式特征——第一个字节作为类型标记用于识别该项是哪种类型的常量,称为“tagbyte”。常量池的索引范围是 1 至 constant_pool_count?1。
Jdk1.6及之前:有永久代, 常量池在方法区 Jdk1.7:有永久代,但已经逐步“去永久代”,常量池在堆 Jdk1.8及之后: 无永久代,常量池在元空间
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
·被模块导出或者开放的包(Package)
·类和接口的全限定名(Fully Qualified Name)
·字段的名称和描述符(Descriptor)
·方法的名称和描述符
·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
方法区
既然提到常量池就顺便提一下方法区,方法区主要保存的信息是类的元数据。方法区与堆空间类似,它也是被JVM中所有的线程共享的区域。方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。类型信息包括类的完整名称、父类的完整名称、类型修饰符(public/protected/private)和类型的直接接口类表。
常量池基本结构
Java类所对应的常量池主要由常量池数量和常量池数组两部分组成(如下图所示),常量池数量紧跟在次版本号的后面,占2字节。常量池数组则紧跟在常量池数量之后。 常量池数组,顾名思义,就是一个类似数组的结构。这个数组固化在字节码文件中,由多个元素组成。与一般数组概念不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度也是不同的,但是每一种元素的第一个数据都是一个u1类型,该字节是标志位,占1个字节(如图4.4所示)。JVM解析常量池时,根据这个u1类型来获取该元素的具体类型。常量池的结构组成如图所示。 使用结构化的方式来描述常量池数组的编排,可以如下描述: tag1元素内容1 tag2元素内容2 tag3元素内容3 … tagn元素内容n 这里为了描述,在tag与元素内容之间添加了空格,但实际的class 二进制文件中,这些tag与元素内容之间绝没有任何空格信息,也没有任何其他的多余信息,tag后面紧跟着元素内容,第一个元素内容结束了,紧接着就是第二个元素的tag信息,由此可见,整个字节码文件的格式设计都是非常紧凑的。
JVM 所定义的11种常量
常量池元素中的不同元素结构与类型都是不同的,正因如此,JVM只能定义有限的元素类型,并针对有限的类型进行专门解析。JVM一共定义了11种常量, JVM常量池元素一览表
常量池元素的复合结构
常量池数组中的每一种元素的内容都是复合数据结构的,下面分别给出JVM所定义的常量池中每一种元素的具体结构。 该表中tag值为1的常量池元素CONSTANT_Utf8_info,其组成结构为3部分,分别是:tag、length和bytes,其中tag和length的长度分别是u1、u2,即分别占1字节和2字节。而bytes则是字符串的具体内容,其长度是length字节。在字节码文件中,该常量池元素最终所占的字节数是: 1 + 2 + length 其他类型的常量池元素的组成结构类似。 常量池元素结构: 可以看到,类的方法信息、接口和继承信息、属性信息都是定义在NamedAndType_Info中的。
常量池的结束位置
相信有不少读者读到这里,可能潜意识里会突然蹦出这么一个问题:整个字节码文件由多个部分构成,常量池数组只是其中一块,JVM在解析字节码文件时,一定需要分别读取各个部分的字节流,其中也包括常量池数组。但是常量池中有部分元素的值是bytes数组,其长度是随机变化的,那么JVM在解析时,是如何知道整个常量池的信息解析到什么位置结束呢?其实经过分析不难发现,首先,class文件给出了常量池的总数;其次,凡是碰到有bytes数组的常量池元素,class文件在常量池的每一个元素之前都会专门划分出2字节用于描述该常量池元素内容所占的字节长度,这样一来,常量池中每一个元素的长度是确定的,而常量池的总数也是确定的,JVM据此便可以从class文件中准确地计算出常量池结构体的末端位置(起始位置不用计算,是定死的,从第9字节开始,前面8字节分别是魔数和版本号)。
常量池元素总数量
前面8字节用于描述魔数和版本号,从第9字节开始的一大段字节流都用于描述常量池数组信息。其中,第9和第10字节用于描述常量池元素的总数量。
第9和第10字节所保存的常量池数组大小是0x33,换算成十进制是51,说明该字节码文件中一共包含51个常量池元素。JVM规定,不使用第0个元素,因此实际上一共有50个常量池元素(下文在解析源码时,会看到源码中的确是从第1个元素开始解析的,而不是从第0个)。
第一个常量池元素
常量池数量之后(即从第11字节开始),就是常量池数组。每一个常量池元素都以tag位标开始,tag位标都只占1字节长度。如图所示。
第11字节对应的值是7,对照上文所给的常量池11种元素的复合结构可知,tag位标为7代表的是CONSTANT_Class_info,即类或接口的符号引用,这种类型的元素的结构组成如下: ◎ tag位标 占1字节 ◎ index 占2字节 既然tag位标已经占据了字节码文件的第11字节,则接下来的第12和13字节将合起来表示index。如上图所示,这两个字节对应的值为2。
从第11字节开始,到第13字节为止,一个常量池元素就被描述完成。紧接着开始描述第2个常量池元素
第一个常量池元素后面紧跟着的是第二个常量池元素。其第一字节是01,表示这是一个UTF8编码的字符串,其结构是: ◎ tag位,占1字节 ◎ length,占2字节 ◎ bytes,占length字节 如图所示,tag位后面的2字节的值是4,表示bytes占4字节,其值为0x54657374,其中每两字节正好代表一个字符,对应的字符串是Test。 *
父类常量
刚才的第1与第2两个常量用于描述Java类型信息,接下来的第3与第4两个常量则用于描述父类信息。由于Test.Java类并没有显式继承任何类,因此编译后处理成默认继承,即父类是Java.lang.Object。 父类常量的tag位也是07,其类名是java/lang/Object 下面分别给出第3和第4这两个常量在文件中的内容。
图4.9显示,字符串的length值为0x10,即16,于是其后面的16字节都是bytes。由此可以进一步验证,字符串常量的结构由tag、length和bytes组成,bytes的长度由length指定。
变量型常量池元素
常量池中前面4个元素主要用于描述Java类自身的名称和其父类名称,接下来的字节码流则开始描述类中的变量信息,这些变量既包括类的成员变量,也包括类变量(即静态变量)信息。 首先看类成员变量a的信息
图所选中的8字节,一共包含两个常量池元素信息,这两个常量池元素的类型都是字符串,因为其tag位都是1。第一个字符串常量的length是1,其值(即bytes)是0x61,正好对应UTF-8编码的字符a。第二个字符串常量的length也是1,其值是0x49,正好对应UTF-8编码的字符I。在JVM规范中,若变量的类型是I,则表示该变量的实际类型是int。这与上文对变量a的定义一致。 接着看类变量si的定义, 所选中的27字节,一共描述了两个常量池元素,这两个常量池元素的类型也都是字符串。第一个字符串的length为2,其值是0x7369,对应utf-8编码的字符串si。第二个字符串的length为0x13,即19,其值是0x4C 6A 61 76 61 2F 6C 61 6E 67 2F 49 6E 74 65 67 65 72 3E,这一串值是ASCII字符,每2个十六进制数对应一个ASCII字符,这些数字连起来就对应一个字符串,所对应的字符串是Ljava/lang/Integer;。 这两个常量池元素合起来,描述了Test类中的static Integer si这样的类变量。
|