1. 前言
“一次编写,到处运行(Write Once, Run Anywhere)”,因为有虚拟机的机制。
1.1 虚拟机与字节码的关系
“同一份输入,不同的输出”,我们只需要生成一份字节码文件,然后同一份.class字节码文件在不同的操作系统中,由不同的虚拟机生成对应机器码。虚拟机和字节码是Java的两个最底层的原理。
最简单的编译运行流程,实际情况比这个复杂的多
1.2 HelloWorld
万物皆可HelloWorld,字节码也不例外。
- HelloWorld.java
- HelloWorld.class
HelloWorld.java编译后,生成HelloWorld.class字节码文件。这里不仔细分析这份字节码文件的内容,主要是先露露脸,混个眼熟。
使用javap -v命令反编译后:
2. 字节码文件
2.1 基础知识
字节码文件是二进制文件,只不过我们一般打开都是16进制形式的。众所周知在二进制中,8位为一个字节,4位二进制数表示一个16进制数,也就是说两个16进制数表示一个字节,所以上图中一个字符占了半个字节长度。在下文中,u1表示占用一个字节长度,u2为两个字节长度,以此类推。
2.2 文件结构
字节码文件结构的组成元素为:
- 魔数(Magic Number)
- 版本号(Minor&Major Version)
- 常量池(Constant Pool)
- 类访问标记(Access Flags)
- 类索引(This Class)
- 超类索引(Super Class)
- 接口表索引(Interfaces)
- 字段表(Fields)
- 方法表(Methods)
- 属性表(Attributes)
2.2.1 魔数
统一的CAFE BABE,其作用与文件名后缀.java、.png等是一样的作用,标示一种类型的文件,不同的文件以二进制的形式打开都能看到对应的魔数。虚拟机在加载字节码文件时会首先检查魔数。
CAFE BABE的来由:http://mishadoff.com/blog/java-magic-part-2-0xcafebabe/
2.2.2 版本号
第二部分是由副版本号与主版本号组成的版本号,在1.2中的HelloWorld.class中的版本号[0000 003a]能看出来我电脑中用的JDK版本是316 + 101 = 58,由于Java版本是从45开始的,所以算下来是Java14,用java -version 确认下:
一个大版本下还会有多个小版本,根据字节码文件留出来的长度可知,一个大版本下最多可以有[0xffff] + 1 = 16^4 = 65536个小版本。
由于每个新版本都会有新特性,所以老版本的虚拟机不能够兼容新版本的字节码文件,所以进行类加载的时候也会检查版本号是否兼容,假如不兼容会抛出java.lang.UnsupportedClassVersionError
的错误。
2.2.3 常量池
常量池算是字节码中最复杂的数据结构,常量池中会存放一些字符串常量与较大的整数,比较小的与常用的整数则是内嵌在字节码指令中,如iconst_1表示整数1入栈。
常量池的结构由常量池大小与其内容组成,常量池大小的字节码长度为两个字节,所有从这里也可以得知一个类文件中最多只能有65536个常量。
假设常量池大小为N,其中有效索引为1~N-1,0为保留索引,其中常量池则最多有N-1项,为何是最多呢?因为Long与Double的类型的常量占用两个索引位置,所以实际常量项会比N少。
一个常量项的数据结构分为类型tag 与 内容,如下:
目前Java有14种常量类型,这里不展开细讲,有兴趣的同学可以自行了解:
constant type | tag |
---|---|
CONSTANT_Utf8_info | 1 |
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 |
2.2.4 类访问标记
类访问标记用来标识一个类是否为final、abstract等,大小为两个字节,用一个位标记一种类型,目前只用了8个位
其中ACC_SUPER已经弃用,但是为了兼容旧版本的字节码文件,所以还占着这个位置。
2.2.5 类索引、超类索引、接口表索引
这三个部分是用以确认一个类的继承关系,索引指向常量池中的项。例如1.2中,对应的类索引为[0x00015] = 21 索引对应的常量也是一个索引,指向的是一个字符串常量——真正的类名:
超类索引与接口表索引也是类似,这里不多赘述。
2.2.6 字段表
类中定义的静态与非静态字段都会存储在这个集合中,不包括方法中定义的字段。
字段表的结构与常量池相似,由长度与内容组成,如下所示:
每个字段项结构如下所示,由四部分组成:
- access_flags:表示字段的访问标记,是否public、private、static、final 等。
- name_index:字段名的索引值,指向常量池的的字符串常量。
- descriptor_index:字段描述符的索引,指向常量池的字符串常量。
- attributes_count、attribute_info:表示属性的个数和属性集合。
2.2.6.1 字段描述符
描述符 | 类型 |
---|---|
B | byte 类型 |
C | char 类型 |
D | double 类型 |
F | float 类型 |
I | int 类型 |
J | long 类型 |
S | short 类型 |
Z | bool 类型 |
L ClassName ; | 引用类型,“L” + 对象类型的全限定名 + “;” |
[ | 一维数组 |
其中引用类型描述符比较特殊,如String类型表示为:“Ljava/lang/String;” ,由于long类型的L被引用类型占了,所以long类型用了J。
2.2.7 方法表
字段表后面的就是方法表,类中定义的方法都会存放在这里,其结构与字段表结构相似:
每个方法项的结构也与字段长得一摸一样,如下:
- access_flags:表示方法的访问标记,是否public、private、static、final 等。
- name_index:方法名的索引值,指向常量池的的字符串常量。
- descriptor_index:方法描述符的索引,指向常量池的字符串常量。
- attributes_count、attribute_info:表示方法相关属性的个数和属性集合,包含了很多有用的信息,比如方法内部的字节码就是存放在 Code 属性中。
2.2.7.1 方法描述符
表示一个方法所需的参数与返回值,格式为“(参数1类型 参数2类型 参数3类型 …)返回值类型”。例如方法Object foo(int i, double d, Thread t)
的方法描述符为:(IDLjava/lang/Thread;)Ljava/lang/Object;
2.2.8 属性表
属性表不只是在顶层的class文件中出现,从上文中可以得知,字段表与方法表中也有对应的属性表。属性表的结构如下:
各种不同属性有不同的结构,具体属性要具体分析,由于篇幅有限这里只介绍最重要的ConstantValue属性与Code属性。
2.2.8.1 ConstantValue
该属性只会出现在字段的属性表中,其结构如下:
attribute_name_index 是指向常量池中值为 “ConstantValue” 的常量项,ConstantValue 属性的 attribute_length 值恒定为 2,constantvalue_index 指向常量池中具体的常量值索引,根据变量的类型不同 constantvalue_index 指向不同的常量项。
2.2.8.2 Code
方法中最重要的就是方法字节码,其实都包含在Code属性中,Code的属性结构如下所示:
Code 属性表的字段含义如下:
- 属性名索引(attribute_name_index)占两个字节,指向常量池中字符串常量,表示属性的名字,比如这里对应的常量池的字符串常量"Code"。
- 属性长度(attribute_length)占用两个字节,表示属性值大小
- max_stack 表示操作数栈的最大深度,方法执行的任意期间操作数栈的深度都不会超过这个值。它的计算规则是有入栈的指令 stack 增加,有出栈的指令 stack 减少,在整个过程中 stack 的最大值就是 max_stack 的值,增加和减少的值一般都是 1,但也有例外:LONG 和 DOUBLE 相关的指令入栈 stack 会增加 2,VOID 相关的指令则为 0。
- max_locals 表示局部变量表的大小,它的值并不是等于方法中所有局部变量的数量之和。
- code_length 和 code 用来表示字节码相关的信息,其中 code_length 表示字节码指令的长度,占用 4 个字节。code 是一个长度为 code_length 的字节数组,存储真正的字节码指令。
- exception_table_length 和 exception_table 用来表示代码内部的异常表信息,如我们熟知的 try-catch 语法就会生成对应的异常表。
- attributes_count 和 attributes[] 用来表示 Code 属性相关的附属属性,Java 虚拟机规定 Code 属性只能包含这四种可选属性:LineNumberTable、LocalVariableTable、LocalVariableTypeTable、StackMapTable。以LineNumberTable 为例,LineNumberTable 用来存放源码行号和字节码偏移量之间的对应关系,这 LineNumberTable 属于调试信息,不是类文件运行的必需的属性,默认情况下都会生成。如果没有这个属性,那么在调试时没有办法在源码中设置断点,也没有办法在代码抛出异常的时候在错误堆栈中显示出错的行号信息。