Class文件结构
大纲:
n 语言无关性
n 文件结构
– 魔数
– 版本
– 常量池
– 访问符
– 类、超类、接口
– 字段
– 方法
– 属性
语言无关性
文件结构:
总结构表:
u1,u2,u3,u4.表示一个无符号的整数。u1表示这个整数占1个byte,u2两个byte.依此类推。u2通常表示一个索引。
Java虚拟机规范中规定,Class文件格式采用一种类似C语言结构体的伪结构来存储,它只有两种数据类型
无符号数(基本数据类型)
主要用于描述数字、索引引用、数量值、或UTF-8编码构成的字符串;
u1 – 1个字节
u2 – 2个字节
u4 – 4个字节
u8 – 8个字节
表(复合数据类型)
用于描述有层次关系的符合结构的数据;
习惯性以“_info”结尾
类型 | 名称 | 数量 |
u4 | Magic(魔数) | 1 |
u2 | minor_version(Class文件的次版本号) | 1 |
u2 | major_version(Class文件的主版本号) | 1 |
u2 | constant_pool_count(常量池的数量) | 1 |
cp_info | constant_pool(常量池) | constant_pool_count - 1 |
u2 | access_flags(类或者接口的标识符) | 1 |
u2 | this_class(类的常量池索引) | 1 |
u2 | super_class(超类的索引) | 1 |
u2 | interfaces_count(接口的数量) | 1 |
u2 | Interfaces(接口表) | interfaces_count |
u2 | fields_count(类的实例变量和类变量的数量) | 1 |
field_info | fields(字段表) | fields_count |
u2 | methods_count(方法的数量) | 1 |
method_info | Methods(方法表) | methods_count |
u2 | attribute_count(属性表的数量) | 1 |
attribute_info | Attributes(属性表) | attributes_count |
Magic(魔数)和主次版本号
n magic u4
– 0xCAFEBABE[z1]
n minor_version u2
n major_version u2
注意:
· -target 1.1中包含了次版本号,之后就没有次版本号了;
· 从1.1到1.4的语法差异很小,默认的-target使用的都不是自身对应版本;
· 1.5开始过后默认的-target是1.5,所以如果要生成1.4的文件格式则需要加上-source 1.4,之后的JDK使用也如此;
最后:某个JVM能接受的class文件的最大主版本号不能超过对应JDK带相应-target参数编译出来的class文件的版本号。例:1.4的JVM能接受最大的class文件的主版本号不能超过1.4 JDK使用-target 1.4输出的主版本号,即48。因为JDK 1.5默认编译输出-target为1.5,则最终class字节码是49.0,所以1.4的JVM是无法执行和支持JDK 1.5编译输出的字节码的,只有抛出错误。
Java中javac
的参数-source
和-target
· -source
表示当前源代码使用什么版本下的JDK进行编译,例如:javac -source 1.4 TestClass.java表示使用JDK 1.4的语法对当前.java源文件进行编译(我机器安装的JDK为1.8),估计从JDK 1.9开始就不支持1.5/5以及更早版本了;
· -target
表示编译器生成特定版本的Java类文件格式,可指定Class文件格式,例如:javac -source 1.4 -target 1.4 TestClass.java表示使用-source 1.4的语法源代码,生成的最终Class文件格式也是1.4的格式;
-target在使用的时候需要加上-source,否则会产生错误,下边两种错误都是不能正确生成.class字节码文件的:
错误1:(不带-source)默认的-source是1.8,但在输出类文件格式的时候尝试使用1.5的文件格式输出
错误2:(带-source)带上了-source的1.8参数过后尝试用1.5的文件格式输出
警告:(带-source)带上了-source的1.5参数过后尝试用1.7的文件格式输出(编译可通过)
综上所述,-source的版本号必须小于或等于-target的版本号,一旦大于了过后可能导致编译不通过,但这里会有一个问题,从下边的表看来,直接使用低版本输出-target 的方式应该是可行的,但这个用法似乎只适合特定版本的JDK,例如:1.6.0_01可直接使用-target 1.5输出JDK 1.5的字节码文件,我在本机使用1.8的版本输出时就会报错。( -source <= -target )
major_version和minor_version
主版本号是u2类型的无符号数表示。major_version和minor_version主要用来表示当前的虚拟机是否接受当前这种版本的Class文件。不同版本的Java编译器编译的Class文件对应的版本是不一样的。高版本的虚拟机支持低版本的编译器编译的 Class文件结构。比如Java SE 6.0对应的虚拟机支持Java SE 5.0的编译器编译的Class文件结构,反之则不行。
常量池
n constant_pool_count u2
n constant_pool cp_info
– CONSTANT_Utf8 1 UTF-8编码的Unicode字符串
– CONSTANT_Integer 3 int类型的字面值
– CONSTANT_Float 4 float类型的字面值
– CONSTANT_Long 5 long类型的字面值
– CONSTANT_Double 6 double类型的字面值
– CONSTANT_Class 7 对一个类或接口的符号引用
– CONSTANT_String 8 String类型字面值的引用
– CONSTANT_Fieldref 9 对一个字段的符号引用
– CONSTANT_Methodref 10 对一个类中方法的符号引用
– CONSTANT_InterfaceMethodref 11 对一个接口中方法的符号引用
– CONSTANT_NameAndType 12 对一个字段或方法的部分符号引用
constant_pool_count代表Class文件中常量池的数目,由于常量池的计数从1开始,因此常量池的容量是constant_pool_count-1。
第0项常量空出做特殊考虑,为了满足一些指向常量池的索引值在某些特定情况下需要表达“不指向任何一个常量池”的意思。
constant_pool常量池是Class类文件中出现的第一个表类型数据,常量池主要存放两大类常量:
a.字面量(Literal):包括文本字符串、final类型常量值。
b.符号引用(SymbolicReferences):包括类和接口的全限定名、字段的名称和描述符、方 法的名称和描述符。
n u2 constant_pool_count 表示常量池的数量。这里我们需要重点来说一下常量池是什么东西,请大家不要与Jvm内存模型中的运行时常量池混淆了,Class文件中常量池主要存储了字面量以及符号引用,其中字面量主要包括字符串,final常量的值或者某个属性的初始值等等,而符号引用主要存储类和接口的全限定名称,字段的名称以及描述符,方法的名称以及描述符,这里名称可能大家都容易理解,至于描述符的概念,放到下面说字段表以及方法表的时候再说。另外大家都知道Jvm的内存模型中 有堆,栈,方法区,程序计数器构成,而方法区中又存在一块区域叫运行时常量池,运行时常量池中存放的东西其实也就是编译器产生的各种字面量以及符号引用, 只不过运行时常量池具有动态性,它可以在运行的时候向其中增加其它的常量进去,最具代表性的就是String的intern方法。
n cp_info 表示常量池,这里面就存在了上面说的各种各样的字面量和符号引用。放到常量池的中数据项在The Java Virtual Machine Specification Java SE 7 Edition 中一共有14个常量,每一种常量都是一个表,并且每种常量都用一个公共的部分tag来表示是哪种类型的常量。
关于常量池
1.基础知识
常量池中主要存放两大类型常量。
· 字面量【Literal】
· 符号引用【Symbolic References】(详细内容可参考编译原理)
符号引用主要包含三种:
1)类和接口的全限定名(Fully Qualified Name);
2)字段的名称和描述符(Descriptor);
3)方法的名称和描述符;
Java和C/C++语言有一点不同,它没有Link【链接】的步骤。
· C/C++语言一般是把源文件编译成.obj的目标文件,然后“链接”成可执行程序;
· Java则会使用JVM加载.class文件,在加载的时候使用动态链接,也就是说Class文件不会保存方法和字段的最终内存信息,这些符号引用如果不经过转化的话是无法直接被虚拟机使用的。
2.常量项目类型
常量池中每一项常量都是一个表,共有11种结构【除去JDK 1.7之后的CONSTANT_InvokeDynamic和CONSTANT_InvokeDynamicTrans两个】,这11种表的第一位都是一个u1类型的标志位(Tag,1 ~ 12,缺少标志为2的数据类型),表示当前常量的类型。
类型 | 标志 | 描述 |
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
3.使用javap输出常量
JDK中提供了javap工具,该工具主要用于分析字节码,使用下边命令可输出当前字节码文件中的所有常量:
javap -verbose TestClass.class
4.常量类型的结构总表
上边提到了11种常量池的结构信息,那么这里再提供11种常量类型的结构总表,细化到前边提到的数据类型(Tag对应3.2中的表)。
常量 | 项目 | 类型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info | tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 | |
CONSTANT_String_info | tag | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
index | u2 | 指向声明方法的类或接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型CONSTANT_NameAndType的索引项 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值为11 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型CONSTANT_NameAndType的索引项 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引 |
CONSTANT_Utf8
n CONSTANT_Utf8
– tag 1
– length u2
– bytes[length]
一个CONSTANT_Utf8_info是一个CONSTANT_Utf8类型的常量池数据项, 它存储的是一个常量字符串。 常量池中的所有字面量几乎都是通过CONSTANT_Utf8_info描述的。下面我们首先讲解CONSTANT_Utf8_info数据项的存储格式。在前面的文章中, 我们提到, 常量池中数据项的类型由一个整型的标志值(tag)决定, 所以所有常量池类型的info中都必须有一个tag信息, 并且这个tag值位于数据项的第一个字节上。 一个11中常量池数据类型, 所以就有11个tag值表示这11中类型。而CONSTANT_Utf8_info的tag值为1, 也就是说如果虚拟机要解析一个常量池数据项, 首先去读这个数据项的第一个字节的tag值, 如果这个tag值为1, 那么就说明这个数据项是一个CONSTANT_Utf8类型的数据项。 紧挨着tag值的两个字节是存储的字符串的长度length, 剩下的字节就存储着字符串。 所以, 它的格式是这样的:
其中tag占一个字节, length占2个字节, bytes代表存储的字符串, 占length字节。所以, 如果这个CONSTANT_Utf8_info存储的是字符串"Hello", 那么他的存储形式是这样的:
现在我们知道了CONSTANT_Utf8_info数据项的存储形式, 那么CONSTANT_Utf8_info数据项都存储了什么字符串呢? CONSTANT_Utf8_info可包括的字符串主要以下这些:
· 程序中的字符串常量
· 常量池所在当前类(包括接口和枚举)的全限定名
· 常量池所在当前类的直接父类的全限定名
· 常量池所在当前类型所实现或继承的所有接口的全限定名
· 常量池所在当前类型中所定义的字段的名称和描述符
· 常量池所在当前类型中所定义的方法的名称和描述符
· 由当前类所引用的类型的全限定名
· 由当前类所引用的其他类中的字段的名称和描述符
· 由当前类所引用的其他类中的方法的名称和描述符
· 与当前class文件中的属性相关的字符串, 如属性名等
总结一下, 其中有这么五类: 程序中的字符串常量, 类型的全限定名, 方法和字段的名称, 方法和字段的描述符, 属性相关字符串。 程序中的字符串常量不用多说了, 我们经常使用它们创建字符串对象, 属性相关的字符串, 等到讲到class中的属性信息(attibute)时自会提及。 方法和字段的名称也不用多说了 。 剩下的就是类型的全限定名,方法和字段的描述符, 这就是上篇文章中提及的"特殊字符串", 不熟悉的同学可以先读一下上篇文章 深入理解Java Class文件格式(二) 。 还有一点需要说明, 类型的全限定名, 方法和字段的名称, 方法和字段的描述符, 可以是本类型中定义的, 也可能是本类中引用的其他类的。
下面我们通过一个例子来进行说明。 示例源码:
1. package com.jg.zhang;
2.
3. public class Programer extends Person {
4.
5. static String company = "CompanyA";
6.
7. static{
8. System.out.println("staitc init");
9. }
10.
11.
12. String position;
13. Computer computer;
14.
15. public Programer() {
16. this.position = "engineer";
17. this.computer = new Computer();
18. }
19.
20. public void working(){
21. System.out.println("coding...");
22. computer.working();
23. }
24. }
别看这个类简单, 但是反编译后, 它的常量池有53项之多。 在这53项常量池数据项中, 各种类型的数据项都有, 当然也包括不少的CONSTANT_Utf8_info 。 下面只列出反编译后常量池中的CONSTANT_Utf8_info 数据项:
1. #2 = Utf8 com/jg/zhang/Programer //当前类的全限定名
2. #4 = Utf8 com/jg/zhang/Person //父类的全限定名
3. #5 = Utf8 company //company字段的名称
4. #6 = Utf8 Ljava/lang/String; //company和position字段的描述符
5. #7 = Utf8 position //position字段的名称
6. #8 = Utf8 computer //computer字段的名称
7. #9 = Utf8 Lcom/jg/zhang/Computer; //computer字段的描述符
8. #10 = Utf8 <clinit> //类初始化方法(即静态初始化块)的方法名
9. #11 = Utf8 ()V //working方法的描述符
10. #12 = Utf8 Code //Code属性的属性名
11. #14 = Utf8 CompanyA //程序中的常量字符串
12. #19 = Utf8 java/lang/System //所引用的System类的全限定名
13. #21 = Utf8 out //所引用的out字段的字段名
14. #22 = Utf8 Ljava/io/PrintStream; //所引用的out字段的描述符
15. #24 = Utf8 staitc init //程序中的常量字符串
16. #27 = Utf8 java/io/PrintStream //所引用的PrintStream类的全限定名
17. #29 = Utf8 println //所引用的println方法的方法名
18. #30 = Utf8 (Ljava/lang/String;)V //所引用的println方法的描述符
19. #31 = Utf8 LineNumberTable //LineNumberTable属性的属性名
20. #32 = Utf8 LocalVariableTable //LocalVariableTable属性的属性名
21. #33 = Utf8 <init> //当前类的构造方法的方法名
22. #41 = Utf8 com/jg/zhang/Computer //所引用的Computer类的全限定名
23. #45 = Utf8 this //局部变量this的变量名
24. #46 = Utf8 Lcom/jg/zhang/Programer; //局部变量this的描述符
25. #47 = Utf8 working //woking方法的方法名
26. #49 = Utf8 coding... //程序中的字符串常量
27. #52 = Utf8 SourceFile //SourceFile属性的属性名
28. #53 = Utf8 Programer.java //当前类所在的源文件的文件名
上面只列出了反编译结果中常量池中的CONSTANT_Utf8_info数据项。 其中第三列不是javap反编译的输出结果, 而是我加上的注释。 读者可以对比上面的程序源码来看一下, 这样的话, 就可以清楚的看出, 源文件中的各种字符串, 是如何和存放到CONSTANT_Utf8_info中的。
这里要强调一下, 源文件中的几乎所有可见的字符串都存放在CONSTANT_Utf8_info中, 其他类型的常量池项只不过是对CONSTANT_Utf8_info的引用。 其他常量池项, 把引用的CONSTANT_Utf8_info组合起来, 进而可以描述更多的信息。 下面将要介绍的CONSTANT_NameAndType_info就可以验证这个结论。
JVM规范里Class文件的常量池项的类型,有两种东西: CONSTANT_Utf8 CONSTANT_String 后者是String常量的类型,但它并不直接持有String常量的内容,而是只持有一个index,这个index所指定的另一个常量池项必须是一个CONSTANT_Utf8类型的常量,这里才真正持有字符串的内容。
在HotSpot VM中,运行时常量池里, CONSTANT_Utf8 -> Symbol*(一个指针,指向一个Symbol类型的C++对象,内容是跟Class文件同样格式的UTF-8编码的字符串) CONSTANT_String -> java.lang.String(一个实际的Java对象的引用,C++类型是oop)
CONSTANT_Utf8会在类加载的过程中就全部创建出来,而CONSTANT_String则是lazy resolve的,例如说在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。那么在尚未resolve的时候,HotSpot VM把它的类型叫做JVM_CONSTANT_UnresolvedString,内容跟Class文件里一样只是一个index;等到resolve过后这个项的常量类型就会变成最终的JVM_CONSTANT_String,而内容则变成实际的那个oop。
看到这里想必也就明白了, 就HotSpot VM的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在StringTable中并没有相应的引用,在堆中也没有对应的对象产生)
CONSTANT_Integer
n CONSTANT_Integer
– tag 3
– byte u4
public static final int sid=99;
一个常量池中的CONSTANT_Integer_info数据项, 可以看做是CONSTANT_Integer类型的一个实例。 它存储的是源文件中出现的int型数据的值。 同样, 作为常量池中的一种数据类型, 它的第一个字节也是一个tag值, 它的tag值为3, 也就是说, 当虚拟机读到一个tag值为3的数据项时, 就知道这个数据项是一个CONSTANT_Integer_info, 它存储的是int型数值的值。 紧挨着tag的下面4个字节叫做bytes, 就是int型数值的整型值。 它的内存布局如下:
下面以示例代码进行说明, 示例代码如下:
1. package com.jg.zhang;
2.
3. public class TestInt {
4.
5. void printInt(){
6. System.out.println(65535);
7. }
8. }
将上面的类生成的class文件反编译:
1. D:\Workspace\AndroidWorkspace\BlogTest\bin>javap -v -c -classpath . com.jg.zhang.TestInt
下面列出反编译的结果, 由于反编译结果较长, 我们省略了大部分信息:
1. ..................
2. ..................
3.
4.
5. Constant pool:
6.
7.
8. ..................
9. ..................
10.
11.
12. #21 = Integer 65535
13.
14. ..................
15. ..................
16.
17. {
18.
19.
20. ..................
21. ..................
22.
23. void printInt();
24. flags:
25. Code:
26. stack=2, locals=1, args_size=1
27. 0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
28. 3: ldc #21 // int 65535
29. 5: invokevirtual #22 // Method java/io/PrintStream.println:(I)V
30. 8: return
31. LineNumberTable:
32. line 6: 0
33. line 7: 8
34. LocalVariableTable:
35. Start Length Slot Name Signature
36. 0 9 0 this Lcom/jg/zhang/TestInt;
37. }
上面的输出结果中, 保留了printInt方法的反编译结果, 并且保留了常量池中的第21项。 首先看printInt方法反编译结果中的索引为3 的字节码指令:
1. 3: ldc #21 // int 65535
这条ldc指令[z2] , 引用了常量池中的第21项, 而第21项是一个CONSTANT_Integer_info, 并且这个CONSTANT_Integer_info存储的整型值为65535 。
CONSTANT_String
n CONSTANT_String
– tag 8
– string_index u2 (指向CONSTANT_Utf8的索引)
在常量池中, 一个CONSTANT_String_info数据项, 是CONSTANT_String类型的一个实例。 它的作用是存储文字字符串, 可以把他看做是一个存在于class文件中的字符串对象。 同样, 它的第一个字节是tag值, 值为8 , 也就是说, 虚拟机访问一个数据项时, 判断tag值为8 , 就说明访问的数据项是一个CONSTANT_String_info 。 紧挨着tag的后两个字节是一个叫做string_index的常量池引用, 它指向一个CONSTANT_Utf8_info, 这个CONSTANT_Utf8_info存放的才是字符串的字面量。 它的内存布局如下:
举例说明, 如果源文件中的一句代码使用了一个字符串常量, 如下所示:
1. void printStrng(){
2. System.out.println("abcdef");
3. }
那么在这个类的常量池中就会有一个CONSTANT_String_info与之相对应, 反编译结果如下:
1. Constant pool:
2.
3. ..............
4. ..............
5.
6. #21 = String #22 // abcdef
7. #22 = Utf8 abcdef
8.
9. ..............
10. ..............
11.
12. {
13.
14. ..............
15. ..............
16.
17. void printStrng();
18. flags:
19. Code:
20. stack=2, locals=1, args_size=1
21. 0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
22. 3: ldc #21 // String abcdef
23. 5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
24. 8: return
25. LineNumberTable:
26. line 7: 0
27. line 8: 8
28. LocalVariableTable:
29. Start Length Slot Name Signature
30. 0 9 0 this Lcom/jg/zhang/TestInt;
31. }
其中printString方法中索引为3的字节码指令ldc引用常量池中的第21项, 第21项是一个CONSTANT_String_info, 这个位于第21项的CONSTANT_String_info又引用了常量池的第22项, 第22项是一个CONSTANT_Utf8_info, 这个CONSTANT_Utf8_info中存储的字符串是 abcdef 。 引用关系的内存布局如下:
CONSTANT_NameAndType
n CONSTANT_NameAndType
– tag 12
– name_index u2 (名字,指向utf8)
– descriptor_index u2 (描述符类型,指向utf8)
常量池中的一个CONSTANT_NameAndType_info数据项, 可以看做CONSTANT_NameAndType类型的一个实例 。 从这个数据项的名称可以看出, 它描述了两种信息,第一种信息是名称(Name), 第二种信息是类型(Type) 。 这里的名称是指方法的名称或者字段的名称, 而Type是广义上的类型, 它其实描述的是字段的描述符或方法的描述符。 也就是说, 如果Name部分是一个字段名称, 那么Type部分就是相应字段的描述符; 如果Name部分描述的是一个方法的名称, 那么Type部分就是对应的方法的描述符。 也就是说, 一个CONSTANT_NameAndType_info就表示了一个方法或一个字段。
下面先看一下CONSTANT_NameAndType_info数据项的存储格式。 既然是常量池中的一种数据项类型, 那么它的第一个字节也是tag, 它的tag值是12, 也就是说, 当虚拟机读到一个tag为12的常量池数据项, 就可以确定这个数据项是一个CONSTANT_NameAndType_info 。 tag值一下的两个字节叫做name_index, 它指向常量池中的一个CONSTANT_Utf8_info, 这个CONSTANT_Utf8_info中存储的就是方法或字段的名称。 name_index以后的两个字节叫做descriptor_index, 它指向常量池中的一个CONSTANT_Utf8_info, 这个CONSTANT_Utf8_info中存储的就是方法或字段的描述符。 下图表示它的存储布局:
下面举一个实例进行说明, 实例的源码为:
1. package com.jg.zhang;
2.
3. public class Person {
4.
5. int age;
6.
7. int getAge(){
8. return age;
9. }
10. }
这个Person类很简单, 只有一个字段age, 和一个方法getAge 。 将这段代码使用javap工具反编译之后, 常量池信息如下:
1. #1 = Class #2 // com/jg/zhang/Person
2. #2 = Utf8 com/jg/zhang/Person
3. #3 = Class #4 // java/lang/Object
4. #4 = Utf8 java/lang/Object
5. #5 = Utf8 age
6. #6 = Utf8 I
7. #7 = Utf8 <init>
8. #8 = Utf8 ()V
9. #9 = Utf8 Code
10. #10 = Methodref #3.#11 // java/lang/Object."<init>":()V
11. #11 = NameAndType #7:#8 // "<init>":()V
12. #12 = Utf8 LineNumberTable
13. #13 = Utf8 LocalVariableTable
14. #14 = Utf8 this
15. #15 = Utf8 Lcom/jg/zhang/Person;
16. #16 = Utf8 getAge
17. #17 = Utf8 ()I
18. #18 = Fieldref #1.#19 // com/jg/zhang/Person.age:I
19. #19 = NameAndType #5:#6 // age:I
20. #20 = Utf8 SourceFile
21. #21 = Utf8 Person.java
常量池一共有21项, 我们可以看到, 一共有两个CONSTANT_NameAndType_info 数据项, 分别是第#11项和第#19项, 其中第#11项的CONSTANT_NameAndType_info又引用了常量池中的第#7项和第#8项, 被引用的这两项都是CONSTANT_Utf8_info , 它们中存储的字符串常量值分别是 <init> 和 ()V。 其实他们加起来表示的就是父类Object的构造方法。 那么这里为什么会是父类Object的构造方法而不是本类的构造方法呢? 这是因为类中定义的方法如果不被引用(也就是说在当前类中不被调用), 那么常量池中是不会有相应的 CONSTANT_NameAndType_info 与之对应的, 只有引用了一个方法, 才有相应的CONSTANT_NameAndType_info 与之对应。 这也是为什么说CONSTANT_NameAndType_info 是方法的符号引用的一部分的原因。 (这里提到一个新的概念, 叫做方法的符号引用, 这个概念会在后面的博客中进行讲解) 可以看到, 在源码存在两个方法, 分别是编译器默认添加的构造方法和我们自己定义的getAge方法, 因为并没有在源码中显示的调用这两个方法,所以在常量池中并不存在和这两个方法相对应的CONSTANT_NameAndType_info 。 之所以会存在父类Object的构造方法对应的CONSTANT_NameAndType_info , 是因为子类构造方法中会默认调用父类的无参数构造方法。 我们将常量中的其他信息去掉, 可以看得更直观:
下面讲解常量池第#19项的CONSTANT_NameAndType_info , 它引用了常量池第#5项和第#6项, 这两项也是CONSTANT_Utf8_info 项, 其中存储的字符串分别是age和I, 其中age是源码中字段age的字段名, I是age字段的描述符。 所以这个CONSTANT_NameAndType_info 就表示对本类中的字段age的引用。 除去常量池中的其他信息, 可以看得更直观:
和方法相同, 只定义一个字段而不引用它(在源码中表现为不访问这个变量), 那么在常量池中也不会存在和该字段相对应的CONSTANT_NameAndType_info 项。这也是为什么说CONSTANT_NameAndType_info作为字段符号引用的一部分的原因。 (这里提到一个新的概念, 叫做字段的符号引用, 这个概念会在后面的博客中进行讲解) 在本例中之所以会出现这个CONSTANT_NameAndType_info , 是因为在源码的getAge方法中访问了这个字段:
1. int getAge(){
2. return age;
3. }
下面给出这两个CONSTANT_NameAndType_info真实的内存布局图:
和Object构造方法相关的CONSTANT_NameAndType_info的示意图:
和age字段相关的CONSTANT_NameAndType_info示意图:
这两张图能够很好的反映出CONSTANT_NameAndType_info和CONSTANT_Utf8_info 这两种常量池数据项的数据存储方式, 也能够真实的反应CONSTANT_NameAndType_info和CONSTANT_Utf8_info 的引用关系。
CONSTANT_Class
n CONSTANT_Class
– tag 7
– name_index u2 (名字,指向utf8)
常量池中的一个CONSTANT_Class_info, 可以看做是CONSTANT_Class数据类型的一个实例。 他是对类或者接口的符号引用。 它描述的可以是当前类型的信息, 也可以描述对当前类的引用, 还可以描述对其他类的引用。 也就是说, 如果访问了一个类字段, 或者调用了一个类的方法, 对这些字段或方法的符号引用, 必须包含它们所在的类型的信息, CONSTANT_Class_info就是对字段或方法符号引用中类型信息的描述。
CONSTANT_Class_info的第一个字节是tag, 值为7, 也就是说, 当虚拟机访问到一个常量池中的数据项, 如果发现它的tag值为7, 就可以判断这是一个CONSTANT_Class_info 。 tag下面的两个字节是一个叫做name_index的索引值, 它指向一个CONSTANT_Utf8_info, 这个CONSTANT_Utf8_info中存储了CONSTANT_Class_info要描述的类型的全限定名。 全限定名的概念在前面的博文 深入理解Java Class文件格式(二) 中将结果, 不熟悉的同学可以先阅读这篇文章。
此外要说明的是, java中数组变量也是对象, 那么数组也就有相应的类型, 并且数组的类型也是使用CONSTANT_Class_info描述的, 并且数组类型和普通类型的描述有些区别。 普通类型的CONSTANT_Class_info中存储的是全限定名, 而数组类型对应的CONSTANT_Class_info中存储的是数组类型相对应的描述符字符串。
举例说明:
与Object类型对应的CONSTANT_Class_info中存储的是: java/lang/Object
与Object[]类型对应的CONSTANT_Class_info中存储的是: [Ljava/lang/Object;
下面看CONSTANT_Class_info的存储布局:
例如, 如果在一个类中引用了System这个类, 那么就会在这个类的常量池中出现以下信息:
CONSTANT_Fieldref ,CONSTANT_Methodref ,CONSTANT_InterfaceMethodref
n CONSTANT_Fieldref ,CONSTANT_Methodref ,CONSTANT_InterfaceMethodref
– tag 9 ,10, 11
– class_index u2 (指向CONSTANT_Class)
– name_and_type_index u2 (指向CONSTANT_NameAndType)
CONSTANT_Fieldref_info
常量池中的一个CONSTANT_Fieldref_info, 可以看做是CONSTANT_Field数据类型的一个实例。 该数据项表示对一个字段的符号引用, 可以是对本类中的字段的符号引用, 也可以是对其他类中的字段的符号引用, 可以是对成员变量字段的符号引用, 也可以是对静态变量的符号引用, 其中ref三个字母就是reference的简写。 之前的文章中, “符号引用”这个名词出现了很多次, 可能有的同学一直不是很明白, 等介绍完CONSTANT_Fieldref_info, 就可以很清晰的了解什么是符号引用。 下面分析CONSTANT_Fieldref_info中的内容都存放了什么信息。
和其他类型的常量池数据项一样, 它的第一个字节也必然是tag, 它的tag值为9 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的tag值为9, 就可以确定这个被访问的数据项是一个CONSTANT_Fieldref_info, 并且知道这个数据项表示对一个字段的符号引用。
tag值下面的两个字节是一个叫做class_index的索引值, 它指向一个CONSTANT_Class_info数据项, 这个数据项表示被引用的字段所在的类型, 包括接口。 所以说, CONSTANT_Class_info可以作为字段符号引用的一部分。
class_index以下的两个字节是一个叫做name_and_type_index的索引, 它指向一个CONSTANT_NameAndType_info, 这个CONSTANT_NameAndType_info前面的博客中已经解释过了, 不明白的朋友可以先看前面的博客:深入理解Java Class文件格式(三) 。 这个CONSTANT_NameAndType_info描述的是被引用的字段的名称和描述符。 我们在前面的博客中也提到过, CONSTANT_NameAndType_info可以作为字段符号引用的一部分。
到此, 我们可以说, CONSTANT_Fieldref_info就是对一个字段的符号引用, 这个符号引用包括两部分, 一部分是该字段所在的类, 另一部分是该字段的字段名和描述符。 这就是所谓的 “对字段的符号引用” 。
下面结合实际代码来说明, 代码如下:
1. package com.jg.zhang;
2.
3. public class TestInt {
4. int a = 10;
5. void print(){
6. System.out.println(a);
7. }
8. }
在print方法中, 引用了本类中的字段a。 代码很简单, 我们一眼就可以看到print方法中是如何引用本类中定义的字段a的。 那么在class文件中, 对字段a的引用是如何描述的呢? 下面我们将这段代码使用javap反编译, 给出简化后的反编译结果:
1. Constant pool:
2. #1 = Class #2 // com/jg/zhang/TestInt
3. #2 = Utf8 com/jg/zhang/TestInt
4.
5. ......
6.
7. #5 = Utf8 a
8. #6 = Utf8 I
9.
10. ......
11.
12. #12 = Fieldref #1.#13 // com/jg/zhang/TestInt.a:I
13. #13 = NameAndType #5:#6 // a:I
14.
15. ......
16.
17. {
18.
19. void print();
20. flags:
21. Code:
22. stack=2, locals=1, args_size=1
23. 0: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream;
24. 3: aload_0
25. 4: getfield #12 // Field a:I
26. 7: invokevirtual #25 // Method java/io/PrintStream.println:(I)V
27. 10: return
28. }
可以看到, print方法的位置为4的字节码指令getfield引用了索引为12的常量池数据项, 常量池中索引为12的数据项是一个CONSTANT_Fieldref_info, 这个CONSTANT_Fieldref_info又引用了索引为1和13的两个数据项, 索引为1的数据项是一个CONSTANT_Class_info, 这个CONSTANT_Class_info数据项又引用了索引为2的数据项, 索引为2的数据项是一个CONSTANT_Utf8_info , 他存储了字段a所在的类的全限定名com/jg/zhang/TestInt 。 而CONSTANT_Fieldref_info所引用的索引为13的数据项是一个CONSTANT_NameAndType_info, 它又引用了两个数据项, 分别为第5项和第6项, 这是两个CONSTANT_Utf8_info , 分别存储了字段a的字段名a, 和字段a的描述符I 。
下面给出内存布局图, 这个图中涉及的东西有点多, 因为CONSTANT_Fieldref_info引用了CONSTANT_Class_info和CONSTANT_NameAndType_info, CONSTANT_Class_info又引用了一个CONSTANT_Utf8_info , 而CONSTANT_NameAndType_info又引用了两个CONSTANT_Utf8_info 。
CONSTANT_Methodref_info
常量池中的一个CONSTANT_Methodref_info, 可以看做是CONSTANT_Methodref数据类型的一个实例。 该数据项表示对一个类中方法的符号引用, 可以是对本类中的方法的符号引用, 也可以是对其他类中的方法的符号引用, 可以是对成员方法字段的符号引用, 也可以是对静态方法的符号引用,但是不会是对接口中的方法的符号引用。 其中ref三个字母就是reference的简写。 在上一小节中介绍了CONSTANT_Fieldref_info, 它是对字段的符号引用, 本节中介绍的CONSTANT_Methodref_info和CONSTANT_Fieldref_info很相似。既然是符号“引用”, 那么只有在原文件中调用了一个方法, 常量池中才有和这个被调用方法的相对应的符号引用, 即存在一个CONSTANT_Methodref_info。 如果只是在类中定义了一个方法, 但是没调用它, 则不会在常量池中出现和这个方法对应的CONSTANT_Methodref_info 。
和其他类型的常量池数据项一样, 它的第一个字节也必然是tag, 它的tag值为10 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的tag值为10, 就可以确定这个被访问的数据项是一个CONSTANT_Methodref_info, 并且知道这个数据项表示对一个方法的符号引用。
tag值下面的两个字节是一个叫做class_index的索引值, 它指向一个CONSTANT_Class_info数据项, 这个数据项表示被引用的方法所在的类型。 所以说, CONSTANT_Class_info可以作为方法符号引用的一部分。
class_index以下的两个字节是一个叫做name_and_type_index的索引, 它指向一个CONSTANT_NameAndType_info, 这个CONSTANT_NameAndType_info前面的博客中已经解释过了, 不明白的朋友可以先看前面的博客:深入理解Java Class文件格式(三) 。 这个CONSTANT_NameAndType_info描述的是被引用的方法的名称和描述符。 我们在前面的博客中也提到过, CONSTANT_NameAndType_info可以作为方法符号引用的一部分。
到此, 我们可以知道, CONSTANT_Methodref_info就是对一个字段的符号引用, 这个符号引用包括两部分, 一部分是该方法所在的类, 另一部分是该方法的方法名和描述符。 这就是所谓的 “对方法的符号引用” 。
下面结合实际代码来说明, 代码如下:
1. package com.jg.zhang;
2.
3. public class Programer {
4.
5. Computer computer;
6.
7. public Programer(Computer computer){
8. this.computer = computer;
9. }
10.
11. public void doWork(){
12. computer.calculate();
13. }
14. }
1. package com.jg.zhang;
2.
3. public class Computer {
4.
5. public void calculate() {
6. System.out.println("working...");
7.
8. }
9. }
上面的代码包括两个类, 其中Programer类引用了Computer类, 在Programer类的doWork方法中引用(调用)了Computer类的calculate方法。源码中对一个方法的描述形式我们再熟悉不过了, 现在我们就反编译Programer, 看看Programer中对Computer的doWork方法的引用, 在class文件中是如何描述的。
下面给出Programer的反编译结果, 其中省去了一些不相关的信息:
1. Constant pool:
2. .........
3.
4.
5. #12 = Utf8 ()V
6.
7.
8. #20 = Methodref #21.#23 // com/jg/zhang/Computer.calculate:()V
9. #21 = Class #22 // com/jg/zhang/Computer
10. #22 = Utf8 com/jg/zhang/Computer
11. #23 = NameAndType #24:#12 // calculate:()V
12. #24 = Utf8 calculate
13.
14. {
15.
16. com.jg.zhang.Computer computer;
17. flags:
18.
19. .........
20.
21. public void doWork();
22. flags: ACC_PUBLIC
23. Code:
24. stack=1, locals=1, args_size=1
25. 0: aload_0
26. 1: getfield #13 // Field computer:Lcom/jg/zhang/Computer;
27. 4: invokevirtual #20 // Method com/jg/zhang/Computer.calculate:()V
28. 7: return
29. }
可以看到, doWork方法的位置为4的字节码指令invokevirtual引用了索引为20的常量池数据项, 常量池中索引为20的数据项是一个CONSTANT_Methodref_info, 这个CONSTANT_Methodref_info又引用了索引为21和23的两个数据项, 索引为21的数据项是一个CONSTANT_Class_info, 这个CONSTANT_Class_info数据项又引用了索引为22的数据项, 索引为22的数据项是一个CONSTANT_Utf8_info , 他存储了被引用的Computer类中的calculate方法所在的类的全限定名com/jg/zhang/Computer 。 而CONSTANT_Methodref_info所引用的索引为23的数据项是一个CONSTANT_NameAndType_info, 它又引用了两个数据项, 分别为第24项和第12项, 这是两个CONSTANT_Utf8_info , 分别存储了被引用的方法calculate的方法名calculate, 和该方法的描述符()V 。
下面给出内存布局图, 这个图中涉及的东西同样有点多, 因为CONSTANT_Methodref_info引用了CONSTANT_Class_info和CONSTANT_NameAndType_info, CONSTANT_Class_info又引用了一个CONSTANT_Utf8_info , 而CONSTANT_NameAndType_info又引用了两个CONSTANT_Utf8_info 。
CONSTANT_InterfaceMethodref_info
常量池中的一个CONSTANT_InterfaceMethodref_info, 可以看做是CONSTANT_InterfaceMethodref数据类型的一个实例。 该数据项表示对一个接口方法的符号引用, 不能是对类中的方法的符号引用。 其中ref三个字母就是reference的简写。 在上一小节中介绍了CONSTANT_Methodref_info, 它是对类中的方法的符号引用, 本节中介绍的CONSTANT_InterfaceMethodref和CONSTANT_Methodref_info很相似。既然是符号“引用”, 那么只有在原文件中调用了一个接口中的方法, 常量池中才有和这个被调用方法的相对应的符号引用, 即存在一个CONSTANT_InterfaceMethodref。 如果只是在接口中定义了一个方法, 但是没调用它, 则不会在常量池中出现和这个方法对应的CONSTANT_InterfaceMethodref 。
和其他类型的常量池数据项一样, 它的第一个字节也必然是tag, 它的tag值为11 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的tag值为11, 就可以确定这个被访问的数据项是一个CONSTANT_InterfaceMethodref, 并且知道这个数据项表示对一个接口中的方法的符号引用。
tag值下面的两个字节是一个叫做class_index的索引值, 它指向一个CONSTANT_Class_info数据项, 这个数据项表示被引用的方法所在的接口。 所以说, CONSTANT_Class_info可以作为方法符号引用的一部分。
class_index以下的两个字节是一个叫做name_and_type_index的索引, 它指向一个CONSTANT_NameAndType_info, 这个CONSTANT_NameAndType_info前面的博客中已经解释过了, 不明白的朋友可以先看前面的博客:深入理解Java Class文件格式(三) 。 这个CONSTANT_NameAndType_info描述的是被引用的方法的名称和描述符。 我们在前面的博客中也提到过, CONSTANT_NameAndType_info可以作为方法符号引用的一部分。
到此, 我们可以知道, CONSTANT_InterfaceMethodref就是对一个接口中的方法的符号引用, 这个符号引用包括两部分, 一部分是该方法所在的接口, 另一部分是该方法的方法名和描述符。 这就是所谓的 “对接口中的方法的符号引用” 。
下面结合实际代码来说明, 代码如下:
1. package com.jg.zhang;
2.
3. public class Plane {
4.
5. IFlyable flyable;
6.
7. void flyToSky(){
8. flyable.fly();
9. }
10. }
1. package com.jg.zhang;
2.
3. public interface IFlyable {
4.
5. void fly();
6. }
在上面的代码中, 定义可一个类Plane, 在这个类中有一个IFlyable接口类型的字段flyable, 然后在Plane的flyToSky方法中调用了IFlyable中的fly方法。 这就是源代码中对一个接口中的方法的引用方式, 下面我们反编译Plane, 看看在class文件层面, 对一个接口中的方法的引用是如何描述的。
下面给出反编译结果, 为了简洁期间, 省略了一些不相关的内容:
1. Constant pool:
2. .........
3.
4. #8 = Utf8 ()V
5.
6. #19 = InterfaceMethodref #20.#22 // com/jg/zhang/IFlyable.fly:()V
7. #20 = Class #21 // com/jg/zhang/IFlyable
8. #21 = Utf8 com/jg/zhang/IFlyable
9. #22 = NameAndType #23:#8 // fly:()V
10. #23 = Utf8 fly
11.
12. {
13.
14. .........
15.
16. com.jg.zhang.IFlyable flyable;
17. flags:
18.
19. .........
20.
21. void flyToSky();
22. flags:
23. Code:
24. stack=1, locals=1, args_size=1
25. 0: aload_0
26. 1: getfield #17 // Field flyable:Lcom/jg/zhang/IFlyable;
27. 4: invokeinterface #19, 1 // InterfaceMethod com/jg/zhang/IFlyable.fly:()V
28. 9: return
29.
30. }
可以看到, flyToSky方法的位置为4的字节码指令invokeinterface引用了索引为19的常量池数据项, 常量池中索引为19的数据项是一个CONSTANT_InterfaceMethodref_info, 这个CONSTANT_InterfaceMethodref_info又引用了索引为20和22的两个数据项, 索引为20的数据项是一个CONSTANT_Class_info, 这个CONSTANT_Class_info数据项又引用了索引为21的数据项, 索引为21的数据项是一个CONSTANT_Utf8_info , 他存储了被引用的方法fly所在的接口的全限定名com/jg/zhang/IFlyable 。 而CONSTANT_InterfaceMethodref_info所引用的索引为22的数据项是一个CONSTANT_NameAndType_info, 它又引用了两个数据项, 分别为第23项和第8项, 这是两个CONSTANT_Utf8_info , 分别存储了被引用的方法fly的方法名fly, 和该方法的描述符()V 。
下面给出内存布局图, 这个图中涉及的东西同样有点多, 因为CONSTANT_InterfaceMethodref_info引用了CONSTANT_Class_info和CONSTANT_NameAndType_info, CONSTANT_Class_info又引用了一个CONSTANT_Utf8_info , 而CONSTANT_NameAndType_info又引用了两个CONSTANT_Utf8_info 。
access flag :类的标示符
n access flag u2:类的标示符
这个标记用于标识类或者接口层次的访问信息,例如:
这个Class是类还是接口?
是否定义为public类型?
是否定义为abstract类型?
如果是类,有没有被声明为final?
注意事项和访问标志计算
其他标记比较容易理解,这里解释一下ACC_SYNTHETIC标记,ACC_SYNTHETIC标记等价的属性称为Synthetic Attribute,它用于指示当前类、接口、方法或字段由编译器生成,而不在源代码中存在(不包含类初始化函数和实例初始化函数),相同的功能还有一种方式就是在类、接口、方法或字段的访问权限中设置ACC_SYNTHETIC标记。Synthetic Attribute是从JDK 1.1中引入的,主要用于支持内嵌类和接口(Nested classes && Interfaces),这些功能目前都可以使用ACC_SYNTHETIC来表达。ACC_SYNTHETIChe Synthetic Attribute功能相同,但不是同一个东西。
access_flags的计算公式为:access_flags = flagA | flagB | flagB ...
比如:一个访问访问标志是0x0021 = 0x0020 | 0x0001 = ACC_SUPER | ACC_PUBLIC
this_class:类
n this_class u2
– 指向常量池的Class
– 表示类的常量池索引,指向常量池中CONSTANT_Class_info的常量
super_class:超类
n super_class u2
– 指向常量池的Class
– 表示超类的索引,指向常量池中CONSTANT_Class_info的常量
interface_count和interfaces:接口数量和接口表
interface_count u2
n interface_count u2
– 接口数量
Interfaces
n interfaces
– interface_count 个 interface u2
– 每个interface是指向CONSTANT_Class的索引
field_count,fields,field:字段数量,字段表,字段
field_count:字段数量
n field_count
– 字段数量
Fields:字段表
n fields
– field_count个field_info
field:字段
n field
access_flags:字段标识符
n access_flags u2
name_index:字段名的索引
n name_index u2
– 常量池引用 ,表示字段的名字
descriptor_index:字段类型描述符
n descriptor_index u2
– 表示字段的类型(前面是描述符,后面是描述符表示的类型)
u B byte
u C char
u D double
u F float
u I int
u J long
u S short
u Z boolean
u V void
u L 对象
l Ljava/lang/Object;
u [
l 数组 [[Ljava/lang/String; = String[][]
attributes_count u2
n attributes_count u2
attribute_info attributes[attributes_count];
n attribute_info attributes[attributes_count];
字段表集合
字段表(field_info)用于描述类或接口中声明的变量,它包含类变量、实例变量,但不包括方法内的局部变量和块变量。和cp_info部分不一样,cp_info因为常量类型的不一样其数据结构有11种,但field_info的结构只有一种。
1.字段表结构
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2.字段访问标志
字段访问标志和类的访问标志算法是一样,但因为修饰字段的标志和修饰类的标志不太一样,看看下边的字段访问标志(上表结构中的access_flags)。
标志名称 | 标志值 | 二进制值 | 含义 |
ACC_PUBLIC | 0x0001 | 0000 0000 0000 0001 | 是否public |
ACC_PRIVATE | 0x0002 | 0000 0000 0000 0010 | 是否private |
ACC_PROTECTED | 0x0004 | 0000 0000 0000 0100 | 是否protected |
ACC_STATIC | 0x0008 | 0000 0000 0000 1000 | 是否static |
ACC_FINAL | 0x0010 | 0000 0000 0001 0000 | 是否final |
ACC_VOLATILE | 0x0040 | 0000 0000 0100 0000 | 是否volatile |
ACC_TRANSIENT | 0x0080 | 0000 0000 1000 0000 | 是否transient |
ACC_SYNTHETIC | 0x1000 | 0001 0000 0000 0000 | 是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 0100 0000 0000 0000 | 是否enum |
3.简单名称、描述符、全限定名
在access_flags标志之后的有两部分:
· name_index:表示字段的简单名称;
· descriptor_index:表示字段和方法的描述符;
这里区分三个概念,本文中反复提到:全限定名、简单名称、描述符:
1. 全限定名
全限定名格式如:com/sco/core/TestClass
,仅仅是把类全名中的.
替换成了/
而已,为了连续多个全限定名不混淆,结尾会使用一个;
表示全限定名结束。
2. 简单名称
简单名称则是没有类型、参数修饰的方法或字段名,比如TestClass类中的age
,name
字段名,inc
方法名。
3. 描述符
方法和字段的描述符主要用来描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)和返回值。
4.字段描述符表
标识字符 | 十六进制值 | 含义 |
B | 42 | 基本类型byte |
C | 43 | 基本类型char |
D | 44 | 基本类型double |
F | 46 | 基本类型float |
I | 49 | 基本类型int |
J | 4A | 基本类型long |
S | 53 | 基本类型short |
Z | 5A | 基本类型boolean |
V | 56 | 基本类型void |
L | 4C | 对象类型,如:Ljava/lang/Object; |
除了上述的基本类型和对象类型描述符以外,Java中还有其他数据类型的描述符:
数组类型:对于数组类型,每一维度使用一个前置的“[
”字符来描述,例:java.lang.String[][] => [[Ljava/lang/String; int[] => [I;
methods_count,methods,method_info:方法数量,方法表,方法
methods_count
n methods_count
– 方法数量
Methods
n methods
– methods_count个method_info
method_info
n method_info
access_flags
n access_flags u2
name_index
n name_index u2
– 方法名字,常量池UTF-8索引
descriptor_index
n descriptor_index u2
– 描述符,用于表达方法的参数和返回值
n 方法描述符
– void inc() ()V
– void setId(int) (I)V
– int indexOf(char[],int ) ([CI)I
attributes_count u2
n attributes_count u2
attribute_info attributes[attributes_count];
n attribute_info attributes[attributes_count];
attribute:属性
n 在field和method中,可以有若干个attribute,类文件也有attribute,用于描述一些额外的信息
– attribute_name_index u2
• 名字,指向常量池UTF-8
– attribute_length u4
• 长度
– info[attribute_length] u1
• 内容
n attribute本身也可以包含其他attribute
n 随着JDK的发展不断有新的attribute加入
Deprecated
n Deprecated
– attribute_name_index u2
– attribute_length u4
n attribute_name_index
– 指向包含Deprecated的UTF-8常量
n attribute_length
– 为0
ConstantValue
n ConstantValue
– attribute_name_index u2
– attribute_length u4
– constantvalue_index u2
n attribute_name_index
– 包含ConstantantValue字面量的UTF-8索引
n attribute_length
– 为2
n constantvalue_index
– 常量值,指向常量池,可以是UTF-8,Float, Double 等
public static final int sid=99;
Code
n Code
有关exception_table 表示,在start_pc和end_pc之间,如果遇到catch_type异常或者它的子异常,则转到handler_pc处理。
LineNumberTable(行号表) - Code属性的属性
n LineNumberTable - Code属性的属性
LocalVariableTable (局部变量表)- Code属性的属性
n LocalVariableTable - Code属性的属性
Exceptions属性
n Exceptions属性
– 和Code属性平级(不要和Code中的Exception table混淆)
– 表示方法抛出的异常(不是try catch部分,而是 throws部分)
n 结构
– attribute_name_index u2
– attribute_length u4
– number_of_exceptions u2
– exception_index_table[number_of_exceptions] u2
• 指向Constant_Class的索引
SourceFile
n SourceFile
– 描述生成Class文件的源码文件名称
n 结构
– attribute_name_index u2
– attribute_length u4
• 固定为2
– soucefile_index u2
• UTF-8常量索引
属性表集合
属性表集合(attribute_info)在前边大部分分析中一直没有遇到,直到TestClass中的方法1中才第一次出现attribute_info的结构。它用于在Class文件、字段表、方法表中携带自己的属性表集合,以用于描述某些场景的专有信息。先看看下边
JVM虚拟机规范预定的属性
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated(过期,弃用)的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | (.java)源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
1.Code属性
属性表结构
对于每个属性,名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构是完全自定义的,但符合规则的属性表应该满足下边的表结构。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u2 | info | attribute_length |
Code属性表结构
Java程序方法体里面的代码经过javac编译器处理过后将最终字节码存储在Code属性内。*:抽象类或接口中的方法不存在Code属性。
Code属性的数据结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
· attribute_name_index:指向CONSTANT_Utf8_info类型常量的索引,常量值固定为“Code
”,即属性的属性名为固定值,长度为4;
· attribute_length:表示该属性的长度,属性长度 = 属性表长度 - 6字节(attribute_name_index占2字节,attribute_length占4字节);
· max_stack:操作数栈(Operand Stack)最大深度,任何时候操作数栈都不会超过这个深度,JVM根据这个值分配栈帧(Frame);
· max_locals:局部变量表所需的存储空间,max_locals单位是Slot(槽),Slot(槽)是JVM为局部变量分配内存所使用的最小单位;
1)对于byte、char、float、int、short、boolean、reference、returnAddress长度不超过32位的数据类型,每个局部变量占用1个Slot;
2)而double和long这两种64位的数据类型则需要占用2个Slot存放
3)另外方法参数(包括实例方法中隐藏参数“this”)、显示异常处理器参数(Exception Handler Parameter,即try-catch语句中catch定义的异常)、方法中定义的局部变量也需要使用局部变量表来存。(Slot中存储的变量是可以重用的,max_locals的大小并不是Slot之和。)
· code_length和code:存储了Java源程序编译后生成的字节码指令,code_length是字节码长度,code是用于存储字节码指令的字节流;每一个字节码指令是u1类型,范围从0x00 ~ 0xFF,包含256条字节容量,而JVM中只有200条编码值对应。code_length是u4类型,理论上最大值可以到(2^32 - 1),但JVM中限制了一个方法不允许超过65535条字节码指令,否则javac会拒绝编译;
Class中核心的两部分:
1. Code:代码区,方法体里的Java代码;
2. Metadata:元数据,类、字段、方法定义以及其他部分信息;
2.Exception属性
异常表(exception_table)结构
类型 | 名称 | 数量 |
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
他的基本含义是:如果字节码从start_pc行(这里的行非源代码行)到第end_pc行(不包含end_pc行)之间出现了类型为catch_type或其子类异常,则转到第handler_pc行继续处理,当catch_type为0时,代表任何异常情况都需要转到handler_pc行处进行处理。
Exception属性结构
和前边的异常表(exception_table)结构不同,这里的Exception属性表是在方法中和Code属性平级,它的作用是列举出方法中可能抛出的受检查异常(Checked Exception),也就是方法描述时在throws关键字后边列举的异常。*:只针对Checked Exception
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
3.LineNumberTable属性
LineNumberTable属性用于描述Java源代码行号和字节码行号(字节码的偏移量[z3] )之间的对应关系,它不是运行时必须属性,但默认会生成到Class文件中。也可以在javac中使用-g:none或-g:lines选项来取消或显示生成这一部分信息。若不生成LineNumberTable的影响就是抛出Exception异常信息的时候不会在堆栈信息中显示行号,并且调试的时候无法按照源代码设置断点。
LineNumberTable属性结构
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
这里只有一点需要说明:line_number_info = start_pc + line_number
,两个变量都是u2类型的变量
· start_pc:表示字节码行号;
· line_number:表示Java源代码行号;
4.LocalVariableTable属性
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源代码定义的变量之间的关系,但是这种关系并非运行时必须,默认也不会生成到Class文件中,可以通过javac中使用-g:none或-g:vars选项取消或者生成这项信息。
如果没有这项信息,最大的影响就是其他人使用这个方法时,所有参数名会丢失,IDE可能使用arg0、arg1占位符替代原来的参数,这对程序运行没有影响,但会给写代码带来很大的不方便。
LocalVariableTable属性结构
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_veriable_info | local_variable_table | local_variable_table_length |
*:这里的特殊变量是local_variable_info类型,它描述了一个栈帧与源代码中局部变量的关联,有单独的结构。
local_variable_info项目结构
类型 | 名称 | 数量 |
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
解释一下五个属性:
· start_pc:表示这个局部变量的生命周期开始的字节码偏移量;
· length:表示这个局部变量的作用范围覆盖长度,和start_pc一起就表示局部变量在字节码中的作用范围;
· name_index:局部变量的名称对应常量池的符号引用;
· descriptor_index:局部变量的类型对应的常量池的符号引用;
· index:局部变量在栈帧局部变量中Slot的位置,如果数据类型是long或double(64bit),Slot为index和index + 1两个位置;
JDK 1.5引入了泛型之后,LocalVeriableTable属性添加了一个“姐妹属性”:LocalVariableTypeTable,这个属性的结构和LocalVariableTable相似,仅仅把记录的字段描述符的descriptor_index替换成字段特征签名(Signature)。
5.SourceFile属性
SourceFile属性主要记录生成这个Class文件的源代码名称,也属于可选属性,可以使用javac的-g:none或-g:source选项来关闭或要求生成这些信息。
SourceFile结构
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
6.ConstantValue属性
字节码中的ConstantValue的主要作用是虚拟机自动为静态变量赋值,只有被修饰了static关键字的变量(类变量)才可以使用这项属性。JVM对static类变量的赋值方式有两种:
· 在类构造器<cinit>中进行——在ConstantValue属性基础之上如果没有final修饰,并且不属于基本类型或java.lang.String,则使用<cinit>;
· 使用ConstantValue属性进行赋值——如果同时使用static和final,并且这个变量数据类型是8种基本类型或java.lang.String,则使用ConstantValue属性初始化;
ConstantValue结构
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
7.InnerClass属性
InnerClass属性结构
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_classes | number_of_classes |
inner_classes_info表结构
类型 | 名称 | 数量 |
u2 | inner_class_info_index | 1 |
u2 | outer_class_info_index | 1 |
u2 | inner_name_index | 1 |
u2 | inner_class_access_flags | 1 |
· inner_name_index:如果是匿名内部类,则这个值为0
inner_class_access_flags标志
标志名称 | 标志值 | 二进制值 | 含义 |
ACC_PUBLIC | 0x0001 | 0000 0000 0000 0001 | 是否public |
ACC_PRIVATE | 0x0002 | 0000 0000 0000 0010 | 是否private |
ACC_PROTECTED | 0x0004 | 0000 0000 0000 0100 | 是否protected |
ACC_STATIC | 0x0008 | 0000 0000 0000 1000 | 是否static |
ACC_FINAL | 0x0010 | 0000 0000 0001 0000 | 是否final |
ACC_SYNCHRONIZED | 0x0020 | 0000 0000 0010 0000 | 是否synchronized |
ACC_ABSTRACT | 0x0400 | 0000 0100 0000 0000 | 是否abstract |
ACC_SYNTHETIC | 0x1000 | 0001 0000 0000 0000 | 是否由编译器自动产生 |
ACC_ANNOTATION | 0x2000 | 0010 0000 0000 0000 | (JDK 1.5之后定义)标识这是一个注解 |
ACC_ENUM | 0x4000 | 0100 0000 0000 0000 | (JDK 1.5之后定义)标识这是一个枚举 |
8.Deprecated, Synthetic属性
Deprecated和Synthetic两个属性都是boolean标记,只存在有和没有的区别,没有属性值的概念,它们结构很简单
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
9.JDK 1.5和JDK 1.6中的新属性
属性名称 | 使用位置 | 含义 |
StackMapTable | Code属性 | JDK 1.6中添加,为了加快Class文件校验,把类型校验时需要用的相关信息直接写入class文件,以前这些信息通过代码数据流分析得到。 |
EnclosingMethod(封闭/包含方法) | 类 | JDK 1.5中添加的属性,当一个类为局部类或匿名类时,可通过此属性声明其访问范围。 |
Signature(签名) | 类、方法表、字段表 | JDK 1.5中添加的属性,存储类、方法、字段的特征签名。JDK 1.5引入泛型是Java语言的进步,虽然使用了类型擦除避免在字节码级别产生冲突,但元数据中的泛型信息需要保留,这种情况下描述符无法精确描述泛型信息,所以添加这个特征签名属性。 |
SourceDebugExtension(源代码调试扩展) | 类 | JDK 1.6中添加的属性,SourceDebugExtension属性用于存储额外调试信息,譬如JSP调试无法通过Java堆栈定位JSP的行号,JSR-45中为了非Java语言编写却需要编译成字节码运行在JVM中的程序提供了可进行调试的标准机制,使用SourceDebugExtension可存储这个标准新加入的调试信息。 |
LocalVariableTypeTable | 类 | JDK 1.5中添加的属性,使用特征签名代替描述符,为了引入泛型语法之后能描述泛型参数化类型而添加。 |
RuntimeVisibleAnnotations(运行时可见注解) | 类、方法表、字段表 | JDK 1.5添加的属性,为动态注解提供支持,RuntimeVisibleAnnoations属性用于指明那些注解是运行时可见的。 |
RuntimeInvisibleAnnotations(运行时不可见注解) | 类、方法表、字段表 | JDK 1.5添加的属性,作用和上边作用刚好相反。 |
RuntimeVisibleParameterAnnotations(运行时可见参数注解) | 方法表 | JDK 1.5添加的属性,作用和RuntimeVisiableAnnotations类似,只不过作用对象是参数。 |
RuntimeInvisibleParameterAnnotations(运行时不可见参数注解) | 方法表 | JDK 1.5添加的属性,不解释。 |
AnnotationDefault(注解默认值) | 方法表 | JDK 1.5添加的属性,用于记录注解类元素的默认值 |
class文件结构例子
示例一:
代码:
public class User { private int id; private String name; private int age; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }
}
|
编译后的字节码文件:
示例二:
代码:
字节码:
Javap反编译后的汇编指令:
为了方便截图使用PowerShell输出,路径切换成.\TestClass.class,其他的没有变化。
解析com.sco.core.TestClass
的字节码
1.魔数段(magic)
CA FE BA BE
Class字节码文件的头四个字节称为魔数(Magic Number),唯一的作用是用于确定这个文件是否为一个虚拟机可接受的Class文件,Java字节码文件的魔数段是固定的,就是“咖啡宝贝”。
2.Class文件版本(minor_version major_version)
00 00 00 34
紧跟魔数的4个字节是Class文件的版本号,第5和第6字节是次版本号(Minor Version,这里是00 00
),第7和第8字节是主版本号(Major Version,这里是00 34
),34
是十六进制,对应十进制的52,即JDK 1.8的字节码。参考2.2章节的版本号详细内容,JDK版本从45开始到52,低版本的JVM是不能执行高版本的字节码的,范围是Version.0到Version.65535,比如JDK 1.7可执行的是51.0 ~ 51.65535。
3.常量池(constant_pool)
3.1.常量池入口
00 24
常量池入口是一个u2类型的数据,表示常量池容量计数(constant_pool_count),从1开始计数,24是十六进制,十进制为36,则表示常量池中有35项常量,索引为1 ~ 35,索引0的位置为预留,可表示“不引用任何一个常量池项目”。只有常量池的容量计数是从1开始!!
3.2.常量池内容
索引值和常量的标号对应,从1 ~ 35总共35个常量
常量1:
0A 00 07 00 14 // java/lang/Object.”<init>”:()V
· 0A
——tag值为10,表示第一个常量类型是CONSTANT_Methodref_info;
· 00 07
——#7 声明当前方法类描述符索引值为7;
· 00 14
——#20 当前方法的名称和类型索引值为20;
常量2:
09 00 06 00 15 // com/sco/core/TestClass.age:I
· 09
——tag值为9,类型为CONSTANT_Fieldref_info;
· 00 06
——#6 声明当前方法类描述符索引值为6;
· 00 15
——#21 字段描述符的名称和类型索引值为21;
常量3:
09 00 06 00 16 // com/sco/core/TestClass.name:Ljava/lang/String;
· 09
——tag值为9,类型为CONSTANT_Fieldref_info;
· 00 06
——#6 声明当前方法类描述符索引值为6;
· 00 16
——#22 字段描述符的名称和类型索引值为22;
除了索引值,和第二个常量的其他内容都一致,也属于字段定义信息。
常量4:
09 00 17 00 18 // java/lang/System.out:Ljava/io/PrintStream;
· 09
——tag值为9,类型为CONSTANT_Fieldref_info;
· 00 17
——#23 声明当前方法类型描述符索引为23;
· 00 18
——#24 字段描述符的名称和类型索引值为24;
常量5:
0A 00 19 00 1A // java/io/PrintStream.out:Ljava/io/PrintStream;
· 0A
——tag值为10,类型为CONSTANT_Methodref_info;
· 00 19
——#25 声明当前方法类描述符索引值为25;
· 00 1A
——#26 当前方法的名称和类型索引值为26;
常量6:
07 00 1B // com/sco/core/TestClass
· 07
——tag值为7,类型为CONSTANT_Class_info;
· 00 1B
——#27 类型为“类或接口符号引用”,所以全限定名常量索引为27;
常量7:
07 00 1C // java/lang/Object
· 07
——tag值为7,类型为CONSTANT_Class_info;
· 00 1C
——#28 类型为“类或接口符号引用”,所以全限定名常量索引为28;
常量8:
01 00 03 61 67 65
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 03
——这个UTF-8编码的常量字符串长度为3,也就是说随后3个字节表示这个字符串常量;
· 61 67 65
——随后3个字节分别表示(字符串“age”)
61 -> 97 -> a
67 -> 103 -> g
65 -> 101 -> e
age
常量9:
01 00 01 49
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 01
——这个UTF-8编码的常量字符串长度为1;
· 49
——随后1个字节表示
49 -> 73 -> I
I
常量10:
01 00 04 6E 61 6D 65
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 04
——这个UTF-8编码的常量字符串长度为4;
· 6E 61 6D 65
——随后四个字节表示(字符串“name”)
6E -> 110 -> n
61 -> 97 -> a
6D -> 109 -> m
65 -> 101 -> e
name
常量11:
01 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 12
——这个UTF-8编码的常量字符串长度为18;
· 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B
——18个字节的字符串,对应:Ljava/lang/String;
常量12:
01 00 06 3C 69 6E 69 74 3E
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 06
——这个UTF-8编码的常量字符串长度为6;
· 3C 69 6E 69 74 3E
——6个字节的字符串,对应:<init>
常量13:
01 00 16 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 49 29 56
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 16
——这个UTF-8编码的常量字符串长度为22;
· 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 49 29 56
——22个字节的字符串,对应:(Ljava/lang/String;I)V
常量14:
01 00 04 43 6F 64 65
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 04
——这个UTF-8编码的常量字符串长度为4;
· 43 6F 64 65
——4个字节的字符串,对应:Code
常量15:
01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 0F
——这个UTF-8编码的常量字符串长度为15;
· 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
——15个字节的字符串,对应:LineNumberTable
常量16:
01 00 03 69 6E 63
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 03
——这个UTF-8编码的常量字符串长度为3;
· 69 6E 63
——3个字节的字符串,对应:inc
常量17:
01 00 03 28 29 49
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 03
——这个UTF-8编码的常量字符串长度为3;
· 28 29 49
——3个字节的字符串,对应:()I
常量18:
01 00 0A 53 6F 75 72 63 65 46 69 6C 65
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 0A
——这个UTF-8编码的常量字符串长度为10;
· 53 6F 75 72 63 65 46 69 6C 65
——10字节的字符串,对应:SourceFile
常量19:
01 00 0E 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 0E
——这个UTF-8编码的常量字符串长度为14;
· 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61
——14字节的字符串,对应:TestClass.java
常量20:
0C 00 0C 00 1D // “lt;initgt;”:()V
· 0C
——tag值为12,类型为CONSTANT_NameAndType_info;
· 00 0C
——#12 该字段或方法名称常量索引为12;
· 00 1D
——#29 该字段或方法描述符常量索引为29;
常量21:
0C 00 08 00 09 // age:I
· 0C
——tag值为12,类型为CONSTANT_NameAndType_info;
· 00 08
——#8 该字段或方法名称常量索引为8;
· 00 09
——#9 该字段或方法描述符常量索引为9;
常量22:
0C 00 0A 00 0B // name:Ljava/lang/String;
· 0C
——tag值为12,类型为CONSTANT_NameAndType_info;
· 00 0A
——#10 该字段或方法名称常量索引为10;
· 00 0B
——#11 该字段或方法描述符常量索引为11;
常量23:
07 00 1E // java/lang/System
· 07
——tag值为7,类型为CONSTANT_Class_info;
· 00 1E
——#30 类型为“类或接口符号引用”,所以全限定名常量索引为30;
常量24:
0C 00 1F 00 20 // out:Ljava/io/PrintStream;
· 0C
——tag值为12,类型为CONSTANT_NameAndType_info;
· 00 1F
——#31 该字段或方法名称常量索引为31;
· 00 20
——#32 该字段或方法描述符常量索引为32;
常量25:
07 00 21 // java/io/PrintStream
· 07
——tag值为7,类型为CONSTANT_Class_info;
· 00 21
——#33 类型为“类或接口符号引用”,所以全限定名常量索引为33;
常量26:
0C 00 22 00 23 // println:(Ljava/lang/System;)V
· 0C
——tag值为12,类型为CONSTANT_NameAndType_info;
· 00 22
——#34 该字段或方法名称常量索引为34;
· 00 23
——#35 该字段或方法描述符常量索引为35;
常量27:
01 00 16 63 6F 6D 2F 73 63 6F 2F 63 6F 72 65 2F 54 65 73 74 43 6C 61 73 73
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 16
——这个UTF-8编码的常量字符串长度为22;
· 63 6F 6D 2F 73 63 6F 2F 63 6F 72 65 2F 54 65 73 74 43 6C 61 73 73
——22字节的字符串,对应:com/sco/core/TestClass
常量28:
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 10
——这个UTF-8编码的常量字符串长度为16;
· 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
——16字节的字符串,对应:java/lang/Object
常量29:
01 00 03 28 29 56
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 03
——这个UTF-8编码的常量字符串长度为3;
· 28 29 56
——3个字节的字符串,对应:()V
常量30:
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 10
——这个UTF-8编码的常量字符串长度为16;
· 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D
——16字节的字符串,对应:java/lang/System
常量31:
01 00 03 6F 75 74
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 03
——这个UTF-8编码的常量字符串长度为3;
· 6F 75 74
——3个字节的字符串,对应:out
常量32:
01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 15
——这个UTF-8编码的常量字符串长度为21;
· 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B
——21个字节的字符串,对应:Ljava/io/PrintStream;
常量33:
01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 13
——这个UTF-8编码的常量字符串长度为19;
· 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D
——19个字节的字符串,对应:java/io/PrintStream
常量34:
01 00 07 70 72 69 6E 74 6C 6E
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 07
——这个UTF-8编码的常量字符串长度为7;
· 70 72 69 6E 74 6C 6E
——7个字节的字符串,对应:println
常量35
01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
· 01
——tag值为1,类型为CONSTANT_Utf8_info;
· 00 15
——这个UTF-8编码的常量字符串长度为21;
· 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
——21个字节的字符串,对应:(L/java/lang/String;)V
上边列举了例子中35个常量的字节码内容,可仔细去对照Class字节文件内容看看常量池的定义信息。常量池的详细信息相对比较繁琐因为每一种常量类型都对应了自己的一种结构,对照上边的详细内容结构表可解析每一个常量的类型、长度、详细内容是什么。
4.访问标志(access_flags)
访问标志:
00 21
该类的访问标志为:0x0021 = 0x0020 | 0x0001 = ACC_SUPER | ACC_PUBLIC
5.类索引、父类索引、接口索引
· 类索引:引用于确定这个类的全限定名;
· 父类索引:引用于确定这个类的父类的全限定名(因为Java语言不支持多继承,所有的类都继承于java.lang.Object,除了java.lang.Object类,所有类的父索引都不为0);
· 接口索引集:接口索引的格式一般格式是:interfaces_count ( u2 ) + interfaces ( u2 ) * n;( n – interfaces_count ),这里interfaces_count表示当前类继承了多少接口,是接口计数器,后边每一个u2类型的正数就是每一个接口的接口索引;
TestClass示例:
00 06 00 07 00 00
· 00 06
:类索引为#6,值:#6 -> #27 -> com/sco/core/TestClass
· 00 07
:父类索引为#7,值:#7 -> #28 -> java/lang/Object
· 00 00
:因为TestClass类没有实现任何接口,所以接口索引集部分为00 00
,并且紧随其后也没有任何字节描述
6.字段表集合
字段计数器:(当前类中有2个字段)
00 02
字段1:
00 82 00 08 00 09 00 00
· 00 82
:access_flags = 0x0080 | 0x0002 = ACC_TRANSIENT | ACC_PRIVATE
· 00 08
:name_index = #8,#8 -> age
· 00 09
:descriptor_index = #9,#9 -> I
· 00 00
:attributes_count:值为0,因为值为0,所以之后自然没有attribute_info部分
字段2:
00 02 00 0A 00 0B 00 00
· 00 02
:access_flags = 0x0002 = ACC_PRIVATE
· 00 0A
:name_index = #10,#10 -> name
· 00 0B
:descriptor_index = #11,#11 -> Ljava/lang/String;
· 00 00
:attributes_count:值为0,所以之后就没有attribute_info部分
7.方法表、Code属性
方法计数器:(当前类中有2个方法)
00 02
7.1.第一个方法
方法1(构造方法):
00 01 00 0C 00 0D 00 01
· 00 01
:access_flags = 0x0001 = ACC_PUBLIC
· 00 0C
:name_index = #12, #12 -> <init>
· 00 0D
:descriptor_index = #13,#13 -> (Ljava/lang/String;I)V
· 00 01
:attributes_count:值为1,所以紧随其后的就是attribute_info部分
方法1的Code(非指令部分):
00 0E 00 00 00 33 00 02 00 03 00 00 00 0F //
非指令部分
· 00 0E
:attribute_name_index = #14,#14 -> Code
· 00 00 00 33
:attribute_length = 33 -> 51,所以整个属性表的长度为51 + 6 = 57字节长度
· 00 02
:max_stack = 2
· 00 03
:max_locals = 3
· 00 00 00 0F
:code_length = 15
方法1的Code(指令部分):
2A B7 00 01 2A 1C B5 00 02 2A 2B B5 00 03 B1 //
指令部分
· 2A B7 00 01
2A -> aload_0
:调用aload_0指令将第一个Reference类型的本地变量推送至栈顶,存储在第0个Slot中;B7 00 01-> invokespecial #1
:调用超类构造方法、实例初始化方法、私有方法,invokespecial之后有一个u2类型的参数,对应<init>的符号引用
· 2A 1C B5 00 02
2A -> aload_0
:调用aload_0指令将第一个Reference类型的本地变量推送至栈顶1C -> iload_2
:调用iload_2将第三个int整型本地变量推送到栈顶;B5 00 02 -> putfield #2
:调用putfield为指定的实例域赋值,00 02的常量为age的符号引用;
· 2A 2B B5 00 03
2A -> aload_0
:调用aload_0指令将第一个Reference类型的本地变量推送至栈顶2B -> aload_1
:调用aload_1指令将第二个Reference类型的本地变量推送至栈顶;B5 00 03 -> putfield #3
:调用putfield为指定的实例域赋值,00 03的常量为name的符号引用;
· B1
:最后一个B1指令为:B1 -> return
表示当前方法返回void,到这里构造函数就调用完成了;
方法1的Exception:
00 00 //
该方法没有
throws
部分的定义
方法1的Attribute Count:
00 01 //
方法
1
最后一部分有一个属性块
方法1的LineNumberTable:
00 0F 00 00 00 12 00 04
00 00 00 07 00 04 00 08 00 09 00 09 00 0E 00 0A
· 00 0F
:attribute_name_index = #15,#15 -> LineNumberTable
· 00 00 00 12
:attribute_length = 14
· 00 04
:line_number_table_length = 4,表示这个LineNumberTable中有4条记录
· 00 00 00 07 00 04 00 08 00 09 00 09 00 0E 00 0A
:Source File -> Byte Code00 00 00 07
-> Source File( 7 ) : Byte Code ( 0 )00 04 00 08
-> Source File( 8 ) : Byte Code ( 4 )00 09 00 09
-> Source File( 9 ) : Byte Code ( 9 )00 0E 00 0A
-> Source File( 14 ) : Byte Code ( 10 )
到这里构造函数的方法1部分的字节码就全部解析完了,接下来看看剩余部分的方法2的字节码。
7.2.第二个方法
方法2:
00 01 00 10 00 11 00 01
· 00 01
:access_flags = 0x0001 = ACC_PUBLIC
· 00 10
:name_index = #16, #16 -> inc
· 00 11
:descriptor_index = #17, #17 -> ()I
· 00 01
:attributes_count:值为1,紧随其后就是attribute_info
方法2的Code(非指令部分):
00 0E 00 00 00 2D 00 02 00 01 00 00 00 11
· 00 0E
:attribute_name_index = #14,#14 -> Code
· 00 00 00 2D
:attribute_length = 2D -> 45,所以整个属性表的长度为45 + 6 = 51字节长度
· 00 02
:max_stack = 2
· 00 01
:max_locals = 1
· 00 00 00 11
:code_length = 17
方法2的Code(指令部分):
B2 00 04 2A B4 00 03 B6 00 05 2A B4 00 02 04 60 AC //
指令部分
· B2 00 04
B2 00 04 -> getstatic #4
:获取指定类的静态域,并且压入到栈顶,这里#4表示指定类的符号引用,为:java/lang/System.out:Ljava/io/PrintStream;
· 2A B4 00 03
2A -> aload_0
:调用aload_0指令将第一个Reference类型的本地变量推送到栈顶B4 00 03 -> getfield #3
:获取指定类的实例域,并且将其压入到栈顶,#3的符号引用为:com/sco/core/TestClass.name:Ljava/lang/String;,即TestClass的实例变量name;
· B6 00 05
B6 00 05 -> invokevirtual #5
:调用实例方法,#5的符号引用为:java/io/PrintStream.println:(Ljava/lang/String;)V
· 2A B4 00 02
2A -> aload_0
:调用aload_0指令将第一个Reference类型的本地变量推送到栈顶B4 00 02 -> getfield #2
:获取指定类的实例域,并且将其压入到栈顶,#2的符号引用为:com/sco/core/TestClass.age:I
· 04 60 AC
04 -> iconst_1
:将int类型的1推送到栈顶60 -> iadd
:将栈顶两个int类型的值相加,返回结果重新推送到栈顶AC -> ireturn
:从当前方法返回int值
方法2的Exception:
00 00 //
该方法没有
throws
部分的定义
方法2的Attribute Count:
00 01 //
方法
1
最后一部分有一个属性块
方法2的LineNumberTable:
00 0F 00 00 00 0A 00 02
00 00 00 0D 00 0A 00 0E
· 00 0F
:attribute_name_index = #15,#15 -> LineNumberTable
· 00 00 00 0A
:attribute_length = 10
· 00 02
:line_number_table_length = 2,表示这个LineNumberTable中有2条记录
· 00 00 00 0D 00 0A 00 0E
:Source File -> Byte Code00 00 00 0D
-> Source File( 13 ) : Byte Code ( 0 )00 0A 00 0E
-> Source File( 14 ) : Byte Code ( 10 )
8.SourceFile属性
00 01 //
方法区过后当前
Class
文件也会包含
attribute
属性信息,当前
Class
文件还有
1
个属性
00 12 00 00 00 02 00 13
· 00 12
:attribute_name_index = #18,#18 -> SourceFile
· 00 00 00 02
:attribute_length = 2
· 00 13
:sourcefile_index = #19, #19 -> TestClass.java
到这里com.sco.core.TestClass这个类的字节码文件就全部解析完成了。
总结:
[z1]Hexspeak(16进制魔术数字)是一种类似Leet(骇客语)的英文单词转写形式。
Hexspeak最早是程序员用来清晰独特地标记内存和数据的一些魔术数字,使用以0-9与A-F构成的16进制数表示一些简单的英文单词。Hexspeak的转写规则为:数字“0”表示字母“O”,“1”表示“I”或“L”,“5”表示“S”,“7”表示“T”,“6”、“9”则各自表示“G”与“g”,其它的数字则可利用画谜和Leet的规则来借代字母,例如“defecate”就可用“DEFECA7E”或“DEFEC8”来表示。
0xCAFEBABE(“cafe babe”)在Mach-O格式文件中用于标识通用二进制目标文件,同时也在Java中用于识别Java字节码类文件。
0xCAFEBABE 的十进制值是 3405691582。 如果我们将每一位的数字相加得到结果是43。一个超过42( 对生命,宇宙和一切的终极答案)。顺便说一下,43岁是一个质数。
[z2]将int、float或String型常量从常量池推送至栈顶
1, [z3]计算机汇编语言中的偏移量定义为:把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。
2, 段地址是针对内存的分段而言的,将每一段的段首地址定义为段地址.段地址的存在是由系统的分段存储决定的,通过段地址和偏移地址就能对数据进行寻访。
3, 偏移地址(SA)是指段内相对于段起始地址的偏移值,例如一个存储器的大小是1KB,可以把它分为4段,第一段的地址范围就是0—255,第二段的地址范围就是256-511,以此类推。这些段内的偏移地址就是在0-255的范围内的。
4, 而计算它们的物理地址只需要把段地址左移4位(得出段首地址),再加上偏移地址就可以了。
5, 偏移量是基于内存分段而出现的概念,将一个存储器的内存分成很多段,将每一段的段首地址定义为段地址,偏移量是指段内相对于段起始地址的偏移值。