[转载更改] Class文件结构--常量池(一) 博客园 访问标志Access_flags
- 这篇文章讲Class格式文件的常量池
- 字节码查看工具:WinHex
1. 常量池
- 先了解常量池中需要存放哪些内容,再讨论用什么类来存放这些内容。
1.1 常量池中存放的内容
-
Class文件中包含常量池,那么我就需要知道常量池会包含哪些内容,接下来才是关心class格式文件用什么类型来存放这些内容。
字面量(Literal): 字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等
符号引用(Symbolic References)
符号引用则属于编译原理方面的概念,包括了下面三类常量:
类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符其它:常量池中主要内容是上面2项,说明还有其它内容,这部分内容,在下面我们看到用来描述常量池内容的14种常量项的介绍时就发现标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。
常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。
1.2 常量池中为什么要包含这些内容
- Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容,在虚拟机类加载过程时再进行详细讲解。
1.3 Class文件中如何描述常量池中内容
知道Class文件的常量池包含的内容后,我们下面就来看看class格式文件使用了哪些类型数据来存放常量池的内容。
由Class文件格式可得紧接着主版本号的是常量池入口。
类型 | 名称 | 数量 |
---|---|---|
u2(无符号数) | constant_pool_count | 1 |
cp_info(表) | constant_pool | constant_pool_count-1 |
占用的字节数:2+(constant_pool_count-1)个具体表所占字节。
由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。
-
先给看一下TestClass.class文件全局的内容,下面就来分析其中常量池中的内容,其它内容后面的文章在分析。从图片也可以看出常量池内容占据了class文件的很大一部分,当然TestClass类中代码比较少就更显得常量池内容的多了。
1.3.1 constant_pool_count
常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项。
设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。
TestClass.class文件中constant_pool_count的十进制值为19,表示常量池中有18项常量,索引范围1-18。
1.3.2 constant_pool
- constant_pool_count表明了后面有多少个常量项。
14种常量项结构
常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。一个常量池中的每个常量项都逃不脱这14种结构。根据下图每个类型的描述我们也可以知道每个类型是用来描述常量池中哪些内容(主要是字面量、符号引用)的。比如:CONSTANT_Integer_info是用来描述常量池中字面量信息的,而且只是整型字面量信息。而标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。
-
-
这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。
这14种常量项结构还有一个特点是,其中13表占用得字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,无法确定大小不固定,编译后,通过utf-8编码,就可以知道其长度。
表 | 占用字节 |
---|---|
CONSTANT_Class_info | 3 |
CONSTANT_Integer_info | 5 |
CONSTANT_Fieldref_info | 5 |
CONSTANT_Methodref_info | 5 |
CONSTANT_Utf8_info | 不固定,取决于length大小 |
1.4 查找testClass.class文件的第一个常量项内容
- 由上面constant_pool_count得到值为19,因为从1开始计数,所以说明后面有18个常量项,由于每个常量项的表结构都不同但是第一位相同,所以读到第一位就可以确定表结构了。下面我们就来查看第一个常量项包含得内容,至于其它17个常量项内容类似,最后还会介绍java提供得一个工具命令javap来帮我们分析class文件字节码内容。
-
由上图可知常量池中第一项常量标志的16进制值是0x0A=10,查表发现这个常量属于CONSTANT_Methodref_info类型,此类型表示类中方法的符号引用。查看该类型的结构如下:
CONSTANT_Methodref_info型常量的第二个数据项为index,类型是u2,index存储的是一个索引值,从class文件中查得该值为oX0004=4,即它指向常量池中第4个常量;第三个数据项也是索引其值为0X000F=15,指向常量池种第15个常量。
- 到此为止,第一个常量项是CONSTANT_Methodref_info型常量项,该类型常量项用来表示类中方法的符号引用,其内容为tag=10,index1=4,index2=15,因为其表示的是类中方法的符号引用,所以index中存放的不是一个具体得内容,而是一个索引位置,所以说其具体内容存放在另一个常量项中。下面我们就来看看其索引指向的常量项(即第4个常量项)的内容到底是什么?
- 找第4个常量项之前需要知道第4个常量项的开始位置,所以需要知道前3个常量项所占字节数。那好就看第2个常量项,由于第一个常量项共占了5个字节,则紧接着的字节就为第二个常量项的tag,如下图可得其值为0X09=9,说明第2个常量项得项目类型为CONSTANT_Fieldref_info。查表得其该类型得字节长度固定占5个字节。
-
依次类推查的第3,4个常量项为CONSTANT_Class_info型。如下图:
-
下面就看第四个常量项CONSTANT_Class_info的内容0X070012。 CONSTANT_Class_info存放的是指向类或接口的符号引用。
根据CONSTANT_Class_info项常量项的结构可知其index数据项又是一个索引项,指向全限定名常量项索引,index数据项的值为0X12=18,表示指向第18个常量项,根据constant_pool_count的值为19可得,常量池中一共有18个常量项,巧了正好在最后一个,但是要知道18个常量项必须知道前17个常量项所占字节,这里就不一一找了,最后找到第18个常量项CONSTANT_Utf8_info在class文件中包含的内容如下:
-
-
根据tag等于1得第18项是CONSTANT_Utf8_info型,该类型存储UTF-8编码的字符串,在TestClass.class文件种该常量项种个数据项的内容如下:
- length(u2):表示UTF-8编码的字符串占用的字节数,值为0x0010=16.
- bytes(u1):表示长度为length的UTF-8编码的字符串.
- 因为length=16,所以 length后面紧跟的长度为16个字节的连续数据是一个使用UTF-8缩略编码表示的字符串。后面紧跟的第一个字节为0x6A=106,那该编码代表的字符为j,我们发现106其实就是字符j对应的ASCII码。后面16个字节代表的字符就是: java/lang/Object
到此为止,我们得到了第一个常量项CONSTANT_Methodref_info的第二个数据项index指向的内容为CONSTANT_Class_info常量项,CONSTANT_Class_info常量的第二个数据项index指向CONSTANT_Utf8_info常量项,CONSTANT_Utf8_info常量项的内容为 java/lang/Object 。
当然CONSTANT_Methodref_info常量项还有第三个数据项index,其存放的也是一个其他常量的索引。
- 根据上面的找法我们就可以找出常量池中包含的内容:字面量和符号引用。
1.5 采用javap命令分析class文件
根据上面的找法我们就可以找出常量池中包含的内容:字面量和符号引用。java考虑到这种找法太麻烦了,所以提供了一个命令javap来帮助我们分析class文件的内容。
javap分析class文件用法:
javap -verbose class文件名
$ javap -verbose TestClass.class
Classfile /E:/studytry/com/zlcook/clazz/TestClass.class
Last modified 2017-4-7; size 292 bytes
MD5 checksum 486567c6d4d7432fc359230fed9c92c7
Compiled from "TestClass.java"
public class com.zlcook.clazz.TestClass
SourceFile: "TestClass.java"
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."":()V
#2 = Fieldref #3.#16 // com/zlcook/clazz/TestClass.m:I
#3 = Class #17 // com/zlcook/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/zlcook/clazz/TestClass
#18 = Utf8 java/lang/Object
{
public com.zlcook.clazz.TestClass();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 2: 0
public int inc();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
}
- 上面通过javap命令得到的结果,该结果显示的很友好,由过上面的理论我们可以很清楚的看到常量池一共18项:其中第一项如下:
#1 = Methodref #4.#15 // java/lang/Object."":()V
- 和我们通过手动方式查看第一个常量项CONSTANT_Methodref_info对比一下就知道javap显示的内容是多么友好了。
第一个常量项 | 第几个 | tag | index | index | 最终代表的内容 |
---|---|---|---|---|---|
class中16进制值 | OX0A | OX004 | OX000F | ||
转换成10进制值 | 10 | 4 | 15 | 查完4和15才知道 | |
javap分析显示的友好值 | #1 | Methodref | #4 | #15 | java/lang/Object." |
1.6 class文件中包含的内容
下面我们来看一下class文件中常量池的内容和java源码中的内容。
TestClass.java代码内容
package com.zlcook.clazz;
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}
- TestClass.class中常量池内容:
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."":()V
#2 = Fieldref #3.#16 // com/zlcook/clazz/TestClass.m:I
#3 = Class #17 // com/zlcook/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/zlcook/clazz/TestClass
#18 = Utf8 java/lang/Object
- 再复习一下常量池中主要存放字面量:如文本字符串、声明为final的常量值等。和符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
- 所以出现com/zlcook/clazz/TestClass、java/lang/Object、m、inc都是应该的,那么I、V、
、LineNumberTable都是什么?那肯定是字段描述符或者是方法描述符了。这部分是编译时自动生成的,它们会被class文件中其它部分(字段表field_info、方法表method_info、属性表attribute_info)引用到,它们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。
2. 哪些字面量会进入常量池中
- 我们知道class文件存放字面量:如文本字符串、声明为final的常量值等。这里的“等”就挺烦人。
- 下面我们来看看哪些字面量会进入常量池。(jdk1.8.0环境)
8种基本类型:
测试案例:
- final类型 FinalTest.java代码
public class FinalTest{
private final int int_num =12;
private final char char_num = 'a';
private final short short_num =30;
private final float float_num = 45.3f;
private final double double_num =39.8;
private final byte byte_num =121;
private final long long_num = 2323L;
private final boolean boolean_flage = true;
}
- 非final类型 test.java代码
public class test{
private int int_num =12;
private char char_num = 'a';
private short short_num =30;
private float float_num = 45.3f;
private double double_num =39.8;
private byte byte_num =121;
private long long_num = 2323L;
private long long_delay_num ;
private boolean boolean_flage = true;
public void init(){
this.long_delay_num = 5555L;
}
}
上面代码测试结果:
- final类型的8种基本类型的值会进入常量池。
- 非final类型的8种基本类型的值double、float、long的值会进入常量池,包括long_delay_num的值。
String类型
- StringTest.java代码:
public class StringTest{
private String str1 = "zl"+"cook";
private String str2 = str1+"hello";
private String str3 = new String("zlcook here?");
private String str4 = "everybody "+ new String("here?");
private final String fin1 = "boy";
private final String fin2 = fin1+ "is boy";
private final String fin3 = str1+ "is boy";
}
-
StringTest.class的常量池种包含内容:
所有测试数据github: 测试数据
3. 访问标志
3.1 示例
C:\Users\Administrator\Desktop>javap -verbose Test.class
Classfile /C:/Users/Administrator/Desktop/Test.class
Last modified 2018-5-13; size 164 bytes
MD5 checksum 0948ee61f2cbaaf5477f8c3bea7b62ce
Compiled from "Test.java"
public interface Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #9 // Test
#2 = Class #10 // java/lang/Object
#3 = Utf8 userName
#4 = Utf8 Ljava/lang/String;
#5 = Utf8 ConstantValue
#6 = String #11 // ADMIN
#7 = Utf8 SourceFile
#8 = Utf8 Test.java
#9 = Utf8 Test
#10 = Utf8 java/lang/Object
#11 = Utf8 ADMIN
{
public static final java.lang.String userName;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String ADMIN
}
SourceFile: "Test.java"
3.2 Access_flags
- 如上面橙色标记,可知访问标志值为:flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT,打开Test.class的字节文件,访问标志值紧跟在常量池之后的两个字节。
-
如上图,访问标志值为:0x0601 ,上面已经得出flags: ACC_PUBLIC,ACC_INTERFACE, ACC_ABSTRACT, 访问标志值0x0601 = 0x0001 or 0x0200 0x0400