【学习JVM】--- 字节码 第一章

一 什么是字节码

java中通过编译, 会将我们程序员编写的文件转换成为一种 .class结尾的文件,我们称之为字节码文件.它是一种二进制文件(很明显是给计算机看的文件),是Java虚拟机中运行的文件.

1.1 如何在IDEA中查看标准的,未经过IDEA反编译过的 .calss文件

(1) javap
IDEA中在 Terminal 终端中,我们通过 javap 命令来查看某个java文件的字节码文件
【学习JVM】--- 字节码 第一章
(2) javap -c
javap -c能够将字节码文件中助记符呈现出来,能够让我们更加详细的看到字节码文件的原貌
【学习JVM】--- 字节码 第一章
(3) javap -verbose
使用 javap -verbose命令分析一个字节码文件时, 将会分析该字节码文件的魔数,版本号,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息;
【学习JVM】--- 字节码 第一章

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进制文件内容

【学习JVM】--- 字节码 第一章

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

【学习JVM】--- 字节码 第一章

访问标志紧跟常量池后面, 有两个字节,

【学习JVM】--- 字节码 第一章

我们可以看到访问标志两个字节是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类中的名称为 aint类型的变量