概述
字节码文件的跨平台性
- Java 语言,跨平台的(write once,run anywhere)
- 当 Java 源代码成功编译成字节码后,如果想在不同的平台上面运行,则无须再次编译
- 这个优势不再那么吸引人了。Python、PHP、Perl、Ruby、Lisp 等有强大的解释器
- 跨平台似乎已经快称为一门语言必选的特性
- Java 虚拟机:跨语言的平台
Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与"Class 文件"这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发, 只要能将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上执行,可以说,统一而强大的 Class 文件结构,就是 Java 虚拟机的基石、桥梁。
官网:
https://docs.oracle.com/javase/specs/index.html
所有的 JVM 全部遵守 Java 虚拟机规范,也就是说所有的 JVM 环境都是一样的, 这样一来字节码文件可以在各种 JVM 上进行。
- 想要让一个 Java 程序正确地运行在 JVM 中,Java 源码就是必须要被编译为符合 JVM 规范的字节码
- 前端编译器的主要任务就是负责将符合 Java 语法规范的 Java 代码转换为符合 JVM 规范的字节码文件
- javac 是一种能够将 Java 源码编译为字节码的前端编译器
- javac 编译器在将 Java 源码编译为一个有效的字节码文件过程中经历了 4 个步骤,分别是 词法分析、语法分析、语义分析以及生成字节码。
Oracle 的 JDK 软件包括两部分内容:
- 一部分是将 Java 源代码编译成 Java 虚拟机的指令集的编译器
- 另一部分是用于实现 Java 虚拟机的运行时环境
Java的前端编译器
前端编译器 VS 后端编译器
Java 源代码的编译结果是字节码,那么肯定需要有一种编译器能够将 Java 源码编译为字节码,承担这个重要责任的就是配置在 path 环境变量中的 javac 编译器。javac 是一种能够将 Java 源码编译为字节码的 前端编译器。
HotSpot VM 并没有强制要求前端编译器只能使用 javac 来编译字节码,其实只要编译结果符合 JVM 规范都可以被 JVM 所识别即可。在 Java 的前端编译器领域,除了 javac 之外,还有一种被大家经常用到的前端编译器,那就是内置在 Eclipse 中的 ECJ (Eclipse Compiler for Java)编译器。和 javac 的全量式编译不同,ECJ 是一种增量式编译器。
- 在 Eclipse 中,当开发人员编写完代码后,使用「Ctrl + S」快捷键时,ECJ 编译器所采取的 编译方案 是把未编译部分的源码逐行进行编译,而非每次都全量编译。因此 ECJ 的编译效率会比 javac 更加迅速和高效,当然编译质量和 javac 相比大致还是一样的
- ECJ 不仅是 Eclipse 的默认内置前端编译器,在 Tomcat 中同样也是使用 ECJ 编译器来编译 jsp 文件。由于 ECJ 编译器是采用 GPLv2 的开源协议进行源代码公开,所以,大家可以登录 Eclipse 官网下载 ECJ 编译器的源码进行二次开发
- 默认情况下,IntelliJ IDEA 使用 javac 编译器(还可以自己设置为 AspectJ 编译器 ajc)
前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给 HotSpot 的 JIT 编译器负责。
透过字节码指令看代码细节
BAT 面试题
- 类文件结构有几个部分
- 知道字节码吗?字节码都有哪些?
Integer x = 5; int y = 5;
比较x == y
都经过哪些步骤
代码举例
例子 1
public class IntegerTest {
public static void main(String[] args) {
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2); // true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false
}
}
查看字节码:
0 bipush 10
2 invokestatic #2
5 astore_1
6 bipush 10
8 invokestatic #2
11 astore_2
12 getstatic #3
15 aload_1
16 aload_2
17 if_acmpne 24 (+7)
20 iconst_1
21 goto 25 (+4)
24 iconst_0
25 invokevirtual #4
28 sipush 128
31 invokestatic #2
34 astore_3
35 sipush 128
38 invokestatic #2
41 astore 4
43 getstatic #3
46 aload_3
47 aload 4
49 if_acmpne 56 (+7)
52 iconst_1
53 goto 57 (+4)
56 iconst_0
57 invokevirtual #4
60 return
单单看字节码,我们发现 10 和 128 没有什么区别,真正的原因在于 Integer 的设计,超出了 128,则重新 new 一个 Integer 对象,所以两个 new 的 Integer 对象使用 ==
是 false
。
输出结果:
true
false
例子 2
public class StringTest {
public static void main(String[] args) {
String str = new String("hello") + new String("world");
String str1 = "helloworld";
System.out.println(str == str1);
}
}
查看字节码:
0 new #2
3 dup
4 invokespecial #3
7 new #4
10 dup
11 ldc #5
13 invokespecial #6
16 invokevirtual #7
19 new #4
22 dup
23 ldc #8
25 invokespecial #6
28 invokevirtual #7
31 invokevirtual #9
34 astore_1
35 ldc #10
37 astore_2
38 getstatic #11
41 aload_1
42 aload_2
43 if_acmpne 50 (+7)
46 iconst_1
47 goto 51 (+4)
50 iconst_0
51 invokevirtual #12
54 return
从字节码的 11、23、35 行我们知道,不相等,因为 31 行的 toString 调用的是 new String()
方法。
输出结果:
false
例子 3
/**
成员变量(非静态的)的赋值过程:
1. 默认初始化
2. 显式初始化 / 代码块中初始化
3. 构造器中初始化
4.有了对象之后,通过[对象.变量]或[对象.方法]对成员变量进行赋值
*/
public class SonTest {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.x);
}
}
class Father {
int x = 10;
public Father() {
this.print();
x = 20;
}
public void print() {
System.out.println("Father.x = " + x);
}
}
class Son extends Father {
int x = 30;
public Son() {
this.print();
x = 40;
}
public void print() {
System.out.println("Son.x = " + x);
}
}
输出结果:
Son.x = 0
Son.x = 30
20
虚拟机的基石 - Class文件
字节码文件里是什么?
源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是 JVM 的指令,而不像 C、C++ 经由编译器直接生成机器码。
什么事字节码指令(byte code)?
Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的 操作码(opcode) 以及跟随其后的零至多个代表此操作所需参数的 操作数(operand) 所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
操作码(操作数),如图 aload_0
是操作码,30 是操作数:
如何解读供虚拟机解释执行的二进制字节码?
方式一:一个一个二进制的看,比如使用 Notepad++,需要安装一个 HEX-Editor 插件,或者使用 Binary Viewer。
方式二:使用 javap -v 文件名.class
指令,JDK 自带的反解析工具。
方式三:使用 IDEA 插件,jclasslib 或 jclasslib bytecode viewer 客户端工具(可视化更好)。
或者
Class文件结构概述
官方文档位置:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
Class 类的本质
任何一个 Class 文件都对应着唯一一个类或接口的定义信息,但反过来说,Class 文件实际上它并不一定以磁盘文件形式存在。Class 文件是一组以 8 位字节为基础单位的 二进制流。
Class 文件格式
Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数 和 表。
- 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以
_info
结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明
代码举例
public class Demo {
private int num = 1;
public int add() {
num = num + 2;
return num;
}
}
对应的字节码文件:
换句话说,充分理解了每一个字节码文件的细节,自己也可以反编译出 Java 源文件来。
Class 文件结构概述
Class 文件的结构并不是一成不变的,随着 Java 虚拟机的不断发展,总是不可避免地会对 Class 文件结构做出一些调整,但是其基本结构和框架是非常稳定的。
Class 文件的总体结构如下:
- 魔数
- Class 文件版本
- 常量池
- 访问标志
- 类索引、父类索引、接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
类型 | 名称 | 说明 | 长度 | 数量 | |
---|---|---|---|---|---|
魔数 | u4 | magic | 魔数,识别 Class 文件格式 | 4 个字节 | 1 |
版本号 | u2 | minor_version | 副版本号(小版本) | 2 个字节 | 1 |
u2 | major_version | 主版本号(大版本) | 2 个字节 | 1 | |
常量池集合 | u2 | constant_pool_count | 常量池计数器 | 2 个字节 | 1 |
cp_info | constant_pool | 常量池表 | n 个字节 | constant_pool_count - 1 | |
访问标识 | u2 | access_flags | 访问标识 | 2 个字节 | 1 |
索引集合 | u2 | this_class | 类索引 | 2 个字节 | 1 |
u2 | super_class | 父类索引 | 2 个字节 | 1 | |
u2 | interfaces_count | 接口计数器 | 2 个字节 | 1 | |
u2 | interfaces | 接口索引集合 | 2 个字节 | interfaces_count | |
字段表集合 | u2 | fields_count | 字段计数器 | 2 个字节 | 1 |
field_info | fields | 字段表 | n 个字节 | fields_count | |
方法表集合 | u2 | methods_count | 方法计数器 | 2 个字节 | 1 |
method_info | methods | 方法表 | n个字节 | methods_count | |
属性表集合 | u2 | attributes_count | 属性计数器 | 2 个字节 | 1 |
attribute_info | attributes | 属性表 | n 个字节 | attributes_count |
魔数
Magic Number(魔数) 。
-
每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number)
-
它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的 Class 文件。即:魔数是 Class 文件的标识符
-
魔数值固定为 0xCAFEBABE。不会改变
-
如果一个 Class 文件不以 0xCAFEBABE 开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:
Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 1885430635 in class file StringTest
-
使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动
魔数也就是俗称的「咖啡 baby」(ca fe ba be)。
Class文件版本号
紧接着魔数的 4 个字节存储的是 Class 文件的版本号。同样也是 4 个字节。第 5 个和第 6 个字节所代表的含义就是编译的副版本号 minor_version,而第 7 个和第 8 个字节就是编译的主版本号 major_version。
它们共同构成了 Class 文件的格式版本号。譬如某个 Class 文件的主版本号为 M,服版本号为 m,那么这个 Class 文件的格式版本号就确定为 M.m
版本号和 Java 编译器的对应关系如下表:
主版本(十进制) | 副版本(十进制) | 编译器版本 |
---|---|---|
45 | 3 | 1.1 |
46 | 0 | 1.2 |
47 | 0 | 1.3 |
48 | 0 | 1.4 |
49 | 0 | 1.5 |
50 | 0 | 1.6 |
51 | 0 | 1.7 |
52 | 0 | 1.8 |
53 | 0 | 1.9 |
54 | 0 | 1.10 |
55 | 0 | 1.11 |
规律:Java 的版本号是从 45 开始的,JDK 1.1 之后的每个 JDK 大版本发布主版本号向上加 1。
不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的。目前,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行由高版本编译器生成的 Class 文件。否则 JVM 会抛出 java.lang.UnsupportedClassVersionError
异常(向下兼容)。
在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时,特别注意开发编译的 JDK 版本和生产环境的 JDK 版本是否一致。
虚拟机 JDK 版本为 1.k (k >= 2)时,对应的 Class 文件格式版本号的范围为 45.0 - 44 + k.0(含两端)。
常量池集合
常量池是 Class 文件中内容最为丰富的区域之一。常量池对于 Class 文件中的字段和方法解析也有着至关重要的作用。
随着 Java 虚拟机的不断发展,常量池的内容也日渐丰富,可以说,常量池是整个 Class 文件的基石。
在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。
常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表 常量池容量计数值(constant_pool_count),与 Java 中语言习惯不一样的是,这个容量计数是从 1 而不是 0 开始的。
类型 | 名称 | 数量 |
---|---|---|
u2(无符号数) | constant_pool_count | 1 |
cp_info(表) | constant_pool | constant_pool_count - 1 |
由上表可见,Class 文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。
- 常量池表项 中,用于存放编译时期生成的各种 字面量 和 符号引用,这部分内容将在类加载后进入方法区的 运行时常量池 中存放
常量池计数器
- 由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值
- 常量池容量计数值(u2 类型):从 1 开始,表示常量池中有多少项常量。即 constant_pool_count = 1 表示常量池中有 0 个常量项
- Demo 的值为:
由图我们得到其值为 0x0016,对应的十进制值为 22。
需要注意的是,Demo 的长度是 22,而这实际上只有 21 项常量。索引为范围是 1 - 21。为什么呢?
通常我们写代码时都是从 0 开始的,但是这里的常量池却是从 1 开始,因为它把第 0 项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达「不引用任何一个常量池项目」的含义,这种情况可用索引值 0 来表示。
常量池表
constant_pool 是一种表结构,以 1 ~ constant_pool_count - 1 为索引。表明了后面有多少个常量项。
常量池主要存放两大类常量:字面量(Literal) 和 符号引用(Symbolic References) 。
它包含了 Class 文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第 1 个字节作为类型标记,用于确定该项的格式,这个字节称为 tag byte(标记字节、标签字节)。
类型 | 标志(或标识) | 描述 |
---|---|---|
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 | 字段或方法的符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
字面量和符号引用
在对这些常量解读前,需要搞清楚几个概念。
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。如下表:
常量 | 具体的常量 |
---|---|
字面量 | 文本字符串 |
字面量 | 声明为 final 的常量值 |
符号引用 | 类和接口的全限定名 |
符号引用 | 字段的名称和描述符 |
符号引用 | 方法的名称和描述符 |
全限定名
com.kele.test.Demo
是全类名,com/kele/test/Demo
是类的全限定名,仅仅是把包名的 .
替换成 /
,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 ;
表示全限定名结束。
简单名称
简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的 add() 方法和 num 字段的简单名称分别是 add 和 num。
描述符
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的 void 类型都用一个大写字符来表示,而对象类型则用字符 L 加对象的全限定名表示,详见下表:
标志符 | 含义 |
---|---|
B | 基本数据类型 byte |
C | 基本数据类型 char |
D | 基本数据类型 double |
F | 基本数据类型 float |
I | 基本数据类型 int |
J | 基本数据类型 long |
S | 基本数据类型 short |
Z | 基本数据类型 boolean |
V | 代表 void 类型 |
L | 对象类型,比如:Ljava/lang/Object; |
[ | 数组类型,一个 [ 代表一维数组。比如:double[][][] is [[[D 是三维数组 |
代码示例:
package cn.kbt;
public class ArrayTest {
public static void main(String[] args) {
Object[] arr = new Object[10];
System.out.println(arr); // [Ljava.lang.Object;@1b6d3586
String[] arr1 = new String[10];
System.out.println(arr1); // [Ljava.lang.String;@4554617c
long[][] arr2 = new long[10][];
System.out.println(arr2); // [[J@74a14482,J 是long,[[ 代表二维数组
}
}
输出结果:
[Ljava.lang.Object;@1b6d3586
[Ljava.lang.String;@4554617c
[[J@74a14482
用描述符来藐视方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号 "()" 之内,如方法 java.lang.String toString()
的描述符为 ()Ljava/lang/String;
,方法 int abc(int[] x ,int y)
描述符为 ([II) I
。
虚拟机在加载 Class 文件时才会进行动态链接,也就是说,Class 文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
这里说明下符号引用和直接引用的区别与关联:
- 符号引用:符号引用以 一组符号 来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中
- 直接引用:直接引用可以是直接指 向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了
常量类型和结构
本内容涉及到解析字节码文件的常量池表,建议看视频学习:
https://www.bilibili.com/video/BV1PJ411n7xZ?p=219
常量池中每一项常量都是一个表,JDK 1.7 之后共 14 种不同的表结构数据。如下表格所示:
根据上图每个类型的描述我们也可以知道每个类型是用来描述常量池中那些内容(主要是字面量、符号引用)的。比如: CONSTANT_Integer_info 是用来描述常量池中字面量信息的,而且只是整型字面量信息。
标志为 15、16、18 的常量项类型是用来支持动态语言调用的(JDK 1.7 时才加入)。
由图我们知道常量池表从 0a 开始,74 结束,右上方的红色箭头代表数组的位置,这里由 21 个箭头,所以数组长度为 21。
上面两张图片对比,我们得到对应的类型信息:
细节说明
-
CONSTANT_Class_info
结构用于表示类或接口 -
CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
和CONSTANT_InterfaceMethodref_info
结构表示字段、方法和接口方法 -
CONSTANT_String_info
结构用于表示 String 类型的常量对象 -
CONSTANT_Integer_info
和CONSTANT_Float_info
表示 4 字节(int 和 float)的数值常量 -
CONSTANT_Long_info
和CONSTANT_Double_info
结构表示 8 字节(long 和 double)的数值常量- 在 Class 文件的常量池中,所有的8字节常量均占两个表成员(项)的空间,如果一个
CONSTANT_Long_info
或CONSTANT_Double_info
结构的项在常量池表中的索引位 n,则常量池表中下一个可用项的索引位 n + 2,此时常量池表中索引为 n + 1,的项仍然有效但必须视为不可用的
- 在 Class 文件的常量池中,所有的8字节常量均占两个表成员(项)的空间,如果一个
-
CONSTANT_NameAndType_info
结构用于表示字段或方法,但是和之前的3个结构不同,CONSTANT_NameAndType_info
结构没有知名该字段或方法所属的类或接口 -
CONSTANT_Utf8_info
用于表示字符常量的值 -
CONSTANT_MethodHandle_info
结构用于表示方法句柄 -
CONSTANT_MethodType_info
结构表示方法类型 -
CONSTANT_InvokeDynamic_info
结构用于表示 invokedynamic 指令所用到的引导方法(bootstrap method)、引导方法所用到的动态调用名称(dynamic invocation name)、参数和返回类型,并可以给引导方法传入一系列称为静态参数(static argument)的常量
解析方式
- 一个字节一个字节的解析
- 使用 javap 命令解析:
javap -verbose Demo.class
或 jclasslib 工具会更方便
总结
-
这 14 种表(或者常量项结构)的共同点是:表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型
-
在常量池列表中,
CONSTANT_Utf8_info
常量项是一种使用改进过的 UTF-8 编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息 -
这 14 种常量项结构还有一个特点是,其中 13 个常量项占用的字节固定,只有
CONSTANT_Utf8_info
占用字节不固定,其大小由 length 决定。为什么?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长去短,所以在没编译前,大小不固定,编译后,通过 UTF-8 编码,就可以知道其长度 -
常量池:可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用 Class 文件空间最大的数据项目之一
-
常量池中为什么包含这些内容
Java 代码在进行 javac 编译的时候,并不像 C 和 C++ 那样有「连接」这一步骤,而是在虚拟机加载 Class 文件的时候进行动态链接。也就是说,在 Class 文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中
访问标识
访问标识(access_flag、访问标志、访问标记)
在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。
各种访问标记如下所示:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 标志为 public 类型 |
ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 |
ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2 之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法) |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应) |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
类的访问权限通常为 ACC_
开头的常量。
每一个种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的。比如,若是 public final 的类,则该标记为 ACC_PUBLIC | ACC_FINAL
。
使用 ACC_SUPER
可以让类更准确地定位到父类的方法 super.method(),现代编译器都会设置并且使用这个标记。
如上图,21 是 0x0021,由 0x0020(ACC_SUPER) + 0x0001(ACC_PUBLIC)组成,所以我们知道,这个类由 public 修饰。
使用 IDEA 插件 jclasslib 查看:
补充说明
带有 ACC_INTERFACE
标志的 Class 文件表示的是接口而不是类,反之则表示的是类而不是接口(两者冲突)
- 如果一个 Class 文件被设置了
ACC_INTERFACE
标志,那么同时也得设置ACC_ABSTRACT
标志。同时它不能再设置ACC_FINAL
、ACC_SUPER
或ACC_ENUM
标志(接口的性质) - 如果没有设置
ACC_INTERFACE
标志,那么这个 Class 问价可以具有上表中除ACC_ANNOTATION
外的其他所有标志。当然,ACC_FINAL
和ACC_ABSTRACT
这类互斥的标志除外。这两个标志不能同时设置
ACC_SUPER
标志用于确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义。针对 Java 虚拟机指令集的编译器都应当设置这个标志。对于 Java SE 8 及后续版本来说,无论 Class 文件中这个标志的实际值是什么,也不管 Class 文件的版本
ACC_SYNTHETIC
标志意味着该类或接口是由编译器生成的,而不是由源代码生成的
注解类型必须设置 ACC_ANNOTATION
标志。如果设置了 ACC_ANNOTATION
标志,那么也必须设置 ACC_INTERFACE
标志
ACC_ENUM
标志标明该类或其父类为枚举类型
表中没有使用的 access_flags 标志是为未来扩充而预留的,这些预留的标志在编译器中应该设置为 0,Java 虚拟机实现也应该忽略他们
类索引、父类索引、接口索引集合
在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:
长度 | 含义 |
---|---|
u2 | this_class |
u2 | super_class |
u2 | interfaces_count |
u2 | interfaces[interfaces_count] |
这三项数据来确定这个类的继承关系:
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0
- 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中
this_class(类索引)
2 字节无符号整数,指向常量池的索引。它提供了类的全限定名,如 com/kele/java1/Demo。this_class 的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为 CONSTANT_Class_info 类型结构体,该结构体表示这个 Class 文件所定义的类或接口。
super_class(父类索引)
2 字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是 java/lang/Object
类。同时,由于 Java 不支持多继承,所以其父类只有一个。
superclass 指向的父类不能是 final。
interfaces
指向常量池索引集合,它提供了一个符号引用到所有已实现的接口。
由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的 CONSTANT_Class(当然这里就必须是接口,而不是类)。
interfaces_count(接口计数器)
interfaces_count 项的值表示当前类或接口的直接超接口数量。
interface[] (接口索引集合)
interfaces[] 中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为 interfaces_count。每个成员 interfaces[i] 必须为 CONSTANT_Class_info 结构,其中 0 JavapTest.txt 将字节码文件内容输出到 JavapTest.txt 文件里。
JavapTest.txt 内容以及对应的注释如下:
Classfile /C:/Users/songhk/Desktop/2/JavapTest.class // 字节码文件所属的路径
Last modified 2020-9-7; size 1358 bytes // 最后修改时间,字节码文件的大小
MD5 checksum 526b4a845e4d98180438e4c5781b7e88 // MD5 散列值
Compiled from "JavapTest.java" // 源文件的名称
public class com.youngkbt.java1.JavapTest
minor version: 0 // 副版本
major version: 52 // 主版本
flags: ACC_PUBLIC, ACC_SUPER // 访问标识
Constant pool: // 常量池
#1 = Methodref #16.#46 // java/lang/Object."":()V
#2 = String #47 // java
#3 = Fieldref #15.#48 // com/youngkbt/java1/JavapTest.info:Ljava/lang/String;
#4 = Fieldref #15.#49 // com/youngkbt/java1/JavapTest.flag:Z
#5 = Fieldref #15.#50 // com/youngkbt/java1/JavapTest.num:I
#6 = Fieldref #15.#51 // com/youngkbt/java1/JavapTest.gender:C
#7 = Fieldref #52.#53 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #54 // java/lang/StringBuilder
#9 = Methodref #8.#46 // java/lang/StringBuilder."":()V
#10 = Methodref #8.#55 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#11 = Methodref #8.#56 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#12 = Methodref #8.#57 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #58.#59 // java/io/PrintStream.println:(Ljava/lang/String;)V
#14 = String #60 // www.youngkbt.com
#15 = Class #61 // com/youngkbt/java1/JavapTest
#16 = Class #62 // java/lang/Object
#17 = Utf8 num
#18 = Utf8 I
#19 = Utf8 flag
#20 = Utf8 Z
#21 = Utf8 gender
#22 = Utf8 C
#23 = Utf8 info
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 COUNTS
#26 = Utf8 ConstantValue
#27 = Integer 1
#28 = Utf8
#29 = Utf8 ()V
#30 = Utf8 Code
#31 = Utf8 LineNumberTable
#32 = Utf8 LocalVariableTable
#33 = Utf8 this
#34 = Utf8 Lcom/youngkbt/java1/JavapTest;
#35 = Utf8 (Z)V
#36 = Utf8 methodPrivate
#37 = Utf8 getNum
#38 = Utf8 (I)I
#39 = Utf8 i
#40 = Utf8 showGender
#41 = Utf8 ()C
#42 = Utf8 showInfo
#43 = Utf8
#44 = Utf8 SourceFile
#45 = Utf8 JavapTest.java
#46 = NameAndType #28:#29 // "":()V
#47 = Utf8 java
#48 = NameAndType #23:#24 // info:Ljava/lang/String;
#49 = NameAndType #19:#20 // flag:Z
#50 = NameAndType #17:#18 // num:I
#51 = NameAndType #21:#22 // gender:C
#52 = Class #63 // java/lang/System
#53 = NameAndType #64:#65 // out:Ljava/io/PrintStream;
#54 = Utf8 java/lang/StringBuilder
#55 = NameAndType #66:#67 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#56 = NameAndType #66:#68 // append:(I)Ljava/lang/StringBuilder;
#57 = NameAndType #69:#70 // toString:()Ljava/lang/String;
#58 = Class #71 // java/io/PrintStream
#59 = NameAndType #72:#73 // println:(Ljava/lang/String;)V
#60 = Utf8 www.youngkbt.com
#61 = Utf8 com/youngkbt/java1/JavapTest
#62 = Utf8 java/lang/Object
#63 = Utf8 java/lang/System
#64 = Utf8 out
#65 = Utf8 Ljava/io/PrintStream;
#66 = Utf8 append
#67 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#68 = Utf8 (I)Ljava/lang/StringBuilder;
#69 = Utf8 toString
#70 = Utf8 ()Ljava/lang/String;
#71 = Utf8 java/io/PrintStream
#72 = Utf8 println
#73 = Utf8 (Ljava/lang/String;)V
// ####################################### 字段表集合的信息 ################################################
{
private int num; // 字段名
descriptor: I // 字段描述符:字段的类型
flags: ACC_PRIVATE // 字段的访问标识
boolean flag;
descriptor: Z
flags:
protected char gender;
descriptor: C
flags: ACC_PROTECTED
public java.lang.String info;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public static final int COUNTS;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 1 // 常量字段的属性:ConstantValue
// ####################################### 方法表集合的信息 ################################################
public com.youngkbt.java1.JavapTest(); // 构造器1的信息
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: ldc #2 // String java
7: putfield #3 // Field info:Ljava/lang/String;
10: return
LineNumberTable:
line 20: 0
line 18: 4
line 22: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/youngkbt/java1/JavapTest;
private com.youngkbt.java1.JavapTest(boolean); // 构造器 2 的信息
descriptor: (Z)V
flags: ACC_PRIVATE
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: ldc #2 // String java
7: putfield #3 // Field info:Ljava/lang/String;
10: aload_0
11: iload_1
12: putfield #4 // Field flag:Z
15: return
LineNumberTable:
line 23: 0
line 18: 4
line 24: 10
line 25: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcom/youngkbt/java1/JavapTest;
0 16 1 flag Z
private void methodPrivate();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 28: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/youngkbt/java1/JavapTest;
int getNum(int);
descriptor: (I)I
flags:
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: getfield #5 // Field num:I
4: iload_1
5: iadd
6: ireturn
LineNumberTable:
line 30: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/youngkbt/java1/JavapTest;
0 7 1 i I
protected char showGender();
descriptor: ()C
flags: ACC_PROTECTED
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #6 // Field gender:C
4: ireturn
LineNumberTable:
line 33: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/youngkbt/java1/JavapTest;
public void showInfo();
descriptor: ()V // 方法描述符:方法的形参列表、返回值类型
flags: ACC_PUBLIC // 方法的访问标识
Code: // 方法的 Code 属性
stack=3, locals=2, args_size=1 //stack:操作数栈的最大深度 locals:局部变量表的长度 args_size:方法接收参数的个数
// 偏移量: 操作码 操作数
0: bipush 10
2: istore_1
3: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
6: new #8 // class java/lang/StringBuilder
9: dup
10: invokespecial #9 // Method java/lang/StringBuilder."":()V
13: aload_0
14: getfield #3 // Field info:Ljava/lang/String;
17: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: iload_1
21: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
24: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
// 行号表:指名字节码指令的偏移量与 java 源程序中代码的行号的一一对应关系
LineNumberTable:
line 36: 0
line 37: 3
line 38: 30
// 局部变量表:描述内部局部变量的相关信息
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this Lcom/youngkbt/java1/JavapTest;
3 28 1 i I
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: ldc #14 // String www.youngkbt.com
2: astore_0
3: return
LineNumberTable:
line 15: 0
line 16: 3
LocalVariableTable:
Start Length Slot Name Signature
}
SourceFile: "JavapTest.java" // 附加属性:指名当前字节码文件对应的源程序文件名
总结
- 通过 javap 命令可以查看一个 Java 类反汇编得到的 Class 文件版本号、常量池、访问标识、变量表、指令代码行号表等信息。不显式类索引、父类索引、接口索引集合、
()
、()
等结构 - 通过对前面的例子代码反汇编文件的简单分析,可以发现,一个方法的执行通常会涉及下面几块内存的操作
- Java 栈中:局部变量表、操作数栈
- Java 堆:通过对象的地址引用去操作
- 常量池
- 其他如帧数据区、方法区的剩余部分等情况,测试中没有显示出来,这里说明一下
- 平常,我们比较关注的是 Java 类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行的,可以参考官方文档查看每个指令含义