【学习JVM】--- 字节码 第一章
一 什么是字节码
java中通过编译, 会将我们程序员编写的文件转换成为一种 .class
结尾的文件,我们称之为字节码文件.它是一种二进制文件(很明显是给计算机看的文件),是Java虚拟机中运行的文件.
1.1 如何在IDEA中查看标准的,未经过IDEA反编译过的 .calss文件
(1) javap
IDEA中在 Terminal 终端中,我们通过 javap 命令来查看某个java文件的字节码文件
(2) javap -c
javap -c
能够将字节码文件中助记符呈现出来,能够让我们更加详细的看到字节码文件的原貌
(3) javap -verbose
使用 javap -verbose
命令分析一个字节码文件时, 将会分析该字节码文件的魔数,版本号,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息;
1.2 Java字节码整体结构
字节数 | 名称 | 含义 |
---|---|---|
4个字节 | Magic Number | 魔数,值为0xCAFEBABE, 是Java的创始人 James Gosling 制定 |
2+2个字节 | Version | 包括minor_version和major_version; minor_version:1.1(45),1.2(46)…1.8(52),指令多年不变,但是版本号每次都发生变化 |
2+n个字节 | Constant Pool | 包括字符串常量和数值常量等 |
2个字节 | This Class Name | 当前类的名字 |
2个字节 | Super Class Name | 父类的名字 |
2+n个字节 | Interfaces | 接口相关的信息(2部分组成:前两个字节表示有几个接口,后n个字节表示具体的每一个接口) |
2+n个字节 | Fields | 当前类的成员变量信息,组成同上 |
2+n个字节 | Methods | 当前类的方法的信息 |
2+n个字节 | Attributes | 当前类的附加的属性 |
普及两个概念
- 字节数据直接量
这是基本的数据类型, 共细分为 u1, u2, u4, u8四种, 分别代表连续的1个字节,2个字节,4个字节,8个字节组成的整体数据;
- 表(数组)
表是由多个基本数据或其他表,按照既定顺序组成的大数据集合. 表是有结构的, 它的结构体现在: 组成表的分析所在的位置和顺序都是已经严格定义好的;
1.3 通过工具WinHex打开字节码文件,查看16进制文件内容
1.4 Java字节码结构解读
- 魔数
所有的
.class
字节码文件的前4个字节都是魔数, 魔数值为固定值:0xCAFEBABE(cafe babe)
- 版本信息
魔数之后的4个字节为版本信息, 前两个字节表示 minor version(次版本号), 后两个字节表示 major_version(主版本号). 这里 1.3中4,5,6,7 四个字节表示版本号,为
00 00 00 34
,换算成10进制,表示次版本号(0000) 为0, 主版本号(0034)为52(52对应java版本为1.8),所以该文件的Java版本号为:1.8.0(这个在1.2的javap -verbose
编译的信息中可以看到)
- 常量池(Constant Pool)
紧接着主版本号之后的就是常量池入口. 一个Java类中定义的很多信息都是由常量池来维护和描述的. 可以将常量池看做是 class文件的资源仓库. 比如说 Java勒种定义的方法与变量信息. 都是存储在常量池中. 常量池中主要存储两类常量: 字面量与符号引用. 字面量如文本字符串, java中声明为final的常量值等, 而符号引用如类和接口的全限定名,字段的名称和描述符,方法的名称和描述符等.
- 常量池的总体结构
Java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成. 常量池数量紧跟在主版本号后面,占据2个字节; 常量池数组则紧跟在常量池数量之后. 常量池数组与一般的数组不同的是, 常量池数组中不同的元素的类型,结构都是不同的,长度当然也就是不同; 但是, 每一种元素的第一个数据都是一个 u1 类型,该字节是个标志位,兼具一个字节. JVM在解析常量池时, 会根据这个 u1 类型来获取元素的具体类型. 指的注意的是,常量池数组中元素的个数=常量池数-1(其中0暂时不使用),目的是满足某些常量池引值的数据在特定情况下需要表达"不引用任何一个常量池"的含义;根本原因就在于索引为0 也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值,所以常量池的索引从1而非0开始;(1.2图示中从#1开始就是这么个意思)
- 描述信息
在 JVM 规范中,每个变量/字段都有描述信息. 描述信息主要的作用是描述字段的数据类型,方法参数列表(包括数量,类型与顺序)与返回值. 根据描述符规则, 基本数据类型和代表无返回值的 void 类型都用一个大写字符 V 来表示, 对象类型则使用字符 L加对象的全限定名来表示. 为了压缩字节码文件的体积,对于基本数据类型, JVM都只使用一个大写字母来表示, 如下所示 ????
B - byte, C - char, D - duble, F - float, I - int, J - long, S - short, Z - boolean, V - void, L - 对象类型,如 Ljava/lang/String
- 数组类型表示方式
对于数组类型来说,每一个维度使用一个前置的
[
来表示,如int[]
被记录为[I
,String[][]
被记录为[[Ljava/lang/String
- 描述符描述方法
用描述符描述方法时, 按照先参数列表, 后返回值的顺序来描述, 参数列表按照参数的严格顺序放在 一组
()
之内,如方法:String getRealnamebyIdAndNickname(int id, String name)
的描述符为:(I,Ljava/lang/String;)Ljava/lang/String
- Access_Flag 访问标志
访问标志信息包括该 Class文件是类还是接口,是否被定义为
public
,是否是abstract
,如果是类,是否声明成final
访问标志紧跟常量池后面, 有两个字节,
我们可以看到访问标志两个字节是
0021
,可是根据图片 Table 4.1. Class access and property modifiers 我们判断 Test1 的访问标志应该为0x0001
因为我们的类是public
类型的,那为什么是0021
呢,原因其实很简单 ????
因为:0x0021是0x0020和0x0001的并集,表示 ACC_PUBLIC与ACC_SUPER
- 当前类的名字(this class name)
占两个字节
跟在 Access_Flag后面的就是 this class name 我们可以看到数值是0003
,这个时候我们根据javap -verbose
编译结果去看常量池#3
的值,发现它引用了#23
(看#3
的注释就很清晰了),很明显#23
的值为该类的名称
- 父类的名称(super class name)
占两个字节
跟在 this class name后面,值为0004
,具体解读方式跟this class name一样
- interfaces 相关接口信息
interfaces由两部分组成(2+n个字节), 前两个字节表示接口的个数,我们看到值为
0000
,就说明没有接口实现,后面的接口描述部分则不存在;
后续补充一个实现了接口的字节码
- 当前类的成员变量信息
2+n个字节, 由两部分组成
fields字段表示用于描述类和接口中声明的变量. 这里的字段包含了类别变量以及实例变量, 但是不包括方法内部声明的局部变量.
前两个字节表示字段的个数,0001
,表示当前类有一个字段;
后面的部分比较复杂,单独分析如下????
第二部分,每一个字段都有自己的一个组成信息,包括以下
类型 | 名称 | 数量 |
---|---|---|
u2(表示两个字节) | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
根据上表我们来结合16进制文件和javap -verbose
信息得到如下结果
名称 | 字节 | 对应verbose信息 |
---|---|---|
access_flags | 0002 | com/turnsole/myjvm/mycalss/Test1 a I
|
name_index | 0005 | a |
descriptor_index | 0006 | I |
attributes_count | 0000 | 说明没有属性 |
attributes | 就不会出现 |
以上表格内容,解读下来就是,
com.turnsole.myjvm.mycalss.Test1
类中的名称为a
的int
类型的变量