JVM:这次一定要搞懂字节码

写在前面

读懂 Class 文件是了解虚拟机运行原理的重要步骤,本文将结合 《深入理解Java虚拟机》中的内容,和大家分享解读 Class 文件的过程。

一、什么是 Class 文件

定义


Class 文件是一组以 8 位字节为基础单位的二进制流

简单地说,Class 文件是由一堆二进制组成的。这些二进制的排列是有一定规则的,虚拟机就是根据这些规则把数据加载到内存中使用的。

实例


我们知道 Class 文件的生成可以由 Java 编译而来。接下来是一个简单的例子:

  1. 首先准备一个 java 文件,里面包含一个属性和方法:
package com.sky.test;

public class Test {
    private int i = 10;
    
    private void test(){
    }
}
  1. ide 编译一下生成 Class 文件,这里用的是 AndroidStudio。早就知道编译后会生成 Class 文件,最后果然在 build 文件夹下找到了 Test.class:

CommonBaseApp\test\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com\sky\test

找到文件后使用 Sublime Text(一个文字编辑器)打开,需要注意的是这里该 Class 文件是用十六进制的格式打开的,里面代表的是十六进制的值:

Test.class

好家伙,这一堆字符。但是不要慌,前面提到过 Class 文件是遵循一定规则的,熟悉了这个规则很容易就看懂了。

二、Class 文件的结构

一张比较经典的图描述了 Class 文件的结构:

Class文件结构

这里的字节实际上是 Java 虚拟机规定的数据类型,1 个字节即 U1、两个字节即 U2 类型。 下文的字节描述对应 U* 类型。

三、Class 文件解析

接下来我们试着用刚才的文件,试着匹配下这个结构图。

Magic Number


Magic Number

魔数:Test.class 开头四个字节 cafe babe 叫做 Magic Number。加上这么一个开头是出于安全性考虑,也是虚拟机识别 Class 文件的重要标识。

Version 版本


Version 版本

后面的四个字节为 0000 0033。这部分前两个字节 0x0000 表示次版本号为 0,主版本号 0x0033 转换为十进制是 51,说明当前文件版本为 51。
这个版本的 Class 文件可以被 JDK 1.7.0 版本以上虚拟机执行,如果用版本不兼容的虚拟机加载 Class 文件会报错。

Constant Pool 常量池


常量池

再往后面的 0x0015 表示常量池元素的数量,0x0015 转换为十进制为 21,说明该常量池有 20 个元素。

这里的下标是从 1 开始的,所以是 20 个元素而不是 21 个。把开始下标 0 置空是为了表达某些情况下 “不引用任何一个常量池项目” 的含义。

#1 常量池第一个元素

接下来就是常量池里面的元素了,紧接着的值是 0x0a=10

常量池第一个元素

我们可以把常量池中的元素看作是一个整体串,其中有 tag 表示其类型、index 表示其指向的常量池下标、length 表示其长度等等。这么一个个整体组成了常量池表。

那么这个 0x0a=10 代表的什么意思呢,看下面的表:

在这里插入图片描述

去找 tag 为 10 的常量,是 CONSTANT_Methodref_info,说明这个元素记录的是一个方法的信息。

[图片上传失败...(image-f0f77-1600151856238)]

说明接下来的数据是一个方法引用的信息,把这个 CONSTANT_Methodref_info 看作是一个类,看这个类的构成:

  • U1(一个字节)的 tag 标记,也就是 0a=10
  • U2(两个字节)的指向该方法的类在常量池中的索引值,也就是这个方法属于哪个类,并指出这个类在常量池中的位置。
    在字节码中是 0x0004=4 说明属于常量池 #4 的元素所表示的类。
  • U2(两个字节)的指向其名称及类描述符的索引值,也就是这个方法的名字以及返回值所在的位置。
    在字节码中是 0x0011=17 说明属于常量池 #17 的元素所表示的信息。

#2 常量池第二个元素

下一个元素的 tag 为 0x09=9

0x09=9

从表中可知为 CONSTANT_Fieldref_info,即字段引用信息。

CONSTANT_Fieldref_info

它也包含三种信息;

  • U1(一个字节) 的 tag 标记,也就是 0x09=9
  • U2(两个字节)的指向该字段所属类或接口的常量池索引,也就是这个字段所属类或接口在常量池中的位置。
    在字节码中为 0x0003=3 说明属于常量池下标为 3 的元素所表示的类。
  • U2(两个字节)的指向该字段描述符的常量池索引,也就是这个字段的名称和描述符在常量池中的位置。
    在字节码中为 0x0012=18 说明指向常量池下标为 18 的元素所表示的信息。

#F 利用工具查看字节码

其实整个 Class 字节码可以通过工具查看,我们直接借用工具来看。IDE 中安装 jclasslib Bytecode viewer,安装重启。选中要查看字节码的类,点这个:

在这里插入图片描述

如果点击提示未找到 class 文件,说明需要编译一下。编译之后再次点击出现如下信息:

在这里插入图片描述

这样这个 Class 文件的所有信息就展示出来了,开始是总览信息,包含 Java 版本、类名、常量池数量等信息。前面我们已经分析过了,这个工具也是根据规则去读取 Class 文件的。

第二个元素的详细信息

前面我们已经知道第二个元素所属类的索引是 3,点到常量池第三个选项可以看到如下信息:

第三个

可以看到工具告诉我们第三个元素是 CONSTANT_Class_info 也就是类信息,并且右边的 Class name 指向了 cp_info #19,也就是常量池的 #19 表示了第二个元素所属的类。可以从工具看到 #19 表示的字符串是 com/sky/test/Test,说明第二个元素属于该类。

回头看第二个元素的字段描述符所在的索引,18。我们就不再去字节码挨个找第 18 个元素了,直接借助工具看:

在这里插入图片描述

第 18 个元素是 CONSTANT_NameAndType_info 类型的,说明是名称和类型的描述:

  • tag:一个字节的 tag,倒推一下是 0x0c=12
  • 两个字节的方法名称索引:0x0005=5
  • 两个字节的方法描述符索引:0x0006=6

如果去字节码去找的话是能找到一串连续的 0c 0005 0006 的,说明倒推的数据也是正确的。

到这里简单捋一捋,第二个元素的方法名和类型指向了 #18,而 #18 的信息又指向了 #5 和 #6。就是说搞清楚 #5 和 #6 就搞清楚第二个元素到底是什么了。

#5 #6

借助工具可知 #5 #6 的类型为 CONSTANT_Utf8_info 也就是一个字符串,字符串是怎么描述的呢,需要再看之前的表:

CONSTANT_Utf8_info
  • tag:一个字节的 tag 0x01=1 表示当前信息是字符串;
  • length:两个字节的信息表示当前字符串的长度;
  • bytes:一个字节的信息表示当前字符串的 ASCII 码。

字节码中找到第 5 个元素如下图:

image.png

0x01 表示字符串,0x0001 表示长度为 1。0x69=69 表示字符串的值,而 69 在 ASCII 码中表示字母 i。

千辛万苦终于找到了个字符串 i,可以猜测是我们之前 java 代码里定义的 int 变量 i。
接着再找字节码的第 6 个元素 010001490x01 代表字符串、0x0001 表示字符串长度为 1、0x49=73 在 ASCII 码中表示大写字母 I。

所以 #5 和 #6 表示的属性类型为 I 说明这是一个 int 类型的值,并且属性名为 i。

#19 的名字

看懂了字符串怎么表示,#19 这个表示类名的字符串也可以看懂了。无非就是 0x01 开头的,后面跟了长度以及一堆跟 ASCII 码对应的数值。这里就不再逐一分析了,直接看答案就好:

#19

描述了 Test 类的全限定名,也就是能表示 Test 类唯一性的路径以及类名。

访问标志


常量池结束后,紧接着两个字节代表访问标志,这个标志用于识别这个 Class 是类还是接口、是 public 还是 abstract。如果是类,是否为 final 等。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为Public
ACC_PRIVATE 0x0002 是否为Private
ACC_PROTECTED 0x0004 是否为Protected
ACC_STATIC 0x0008 是否为static
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义.jdk 1.0.2 以后编译出来的都必须为真.
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举

我们知道 Test 是一个类,并且是高版本 jdk 编译出来的,所以它的 flag 应该是 0x00010x0020。把他们进行或运算,结果为 0x0021。无论是从字节码还是用工具看,都能印证这个结果。

访问标志

类索引

类索引最终表示的是该类的类名。它指向的是常量池中一个 CONSTANT_Class_info 类型的常量,而该常量又指向表示其名称的字符串常量。

类索引

图中标红的位置就是类索引在常量池中的下标,前文分析字段的时候已经知道 #3 为一个 CONSTANT_Class_info 类型,指向表名其名称的字符串为 com/sky/test/Test 正是该类的全限定名。

全限定名

父类索引

父类索引紧随着类索引,字节码中是 0x0004 指向的是 #4。与类索引相同,最终表示为 java/lang/Object。说明 Test 类的父类为 Object。

在这里插入图片描述

接口索引

接口索引在父类索引之后,它的表示为一组 u2 类型的数据的集合。字节码中为 0x0000 说明没有实现接口。如果有接口实现,则先表示数量,然后再指向接口全限定名在常量池中的下标。

假如 Test 实现了两个接口,并且两个接口在常量池中的表示位置为 3 和 4,那么对应的字节码应该是 0002 0003 0004

字段表集合

字段表用于描述接口或者类中声明的变量。

紧接着的字节码表示字段表,字段表的结构如下:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attributes_info attributes attributes_count

[图片上传失败...(image-688c7b-1600151856238)]

  • 0x0001:该 Class 只有一个字段;
  • 0x0002:该字段的访问标志(access_flags),也就是 private,可以参照上文的访问标志表;
  • 0x0005:字段名(name_index),常量池索引 #5,也就是 i;
  • 0x0006:字段描述符(descriptor_index),常量池索引 #6,也就是 I。说明是 int 类型的数据。
  • 0x0000:属性表(attributes_info)为 0,说明不需要额外的信息描述该变量。如果该变量为 final sttic int i = 12,则可能会存在一个名称为 ConstantVaule 的属性,并指向常量 12。

方法表集合

方法表的表现形式与字段表的描述几乎完全一致,方法表结构:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attributes_info attributes attributes_count

接下来就是方法表的内容了,看字节码:

方法表

开头的 0x0002:表示有两个方法,一个是由编译器生成的方法,一个是我们写的 test 方法。

第一个方法

  • 0x0001:access_flag,表示该方法为 public;
  • 0x0007:方法名常量池下标,执行 #7,方法名为 "< init >";
  • 0x0008:方法描述符的索引,对应常量为 "()V" 表示返回 void;
  • 0x0001:该方法有一个属性表;

这里涉及到属性表,上文不止一次提到过属性表。在 Class 文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景的专有信息。

我们接着看后面的字节码:

  • 0x0009:该方法属性表的属性索引,对应常量为 "Code";
  • 0x0000 0039:该方法属性表的长度;
  • 再往后就是 Code 属性的详细描述。

Code 是虚拟机预定义的属性之一,表示接下来是一段由编译器生成的字节码,也就是虚拟机可执行的代码。

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("Code")和 attribute_length("0x0000 0039")已经分析过,接下来根据这个表继续对字节码进行分析。

  • 0x0002:max_stack = 2,操作数栈最大深度,这里是 2。在方法执行的任何时刻,操作数栈都不会超过这个深度。
  • 0x0001:max_locals = 1,局部变量表所需的存储空间。
    局部变量表的单位是 Slot,对于 byte、char、float、int、short、boolean 和 returnAddress 等长度不超过 32 位的数据类型,每个局部变量占用 1 个 Slot。double 和 long 这两种 64 位的数据类型需要两个 Slot 来存放。
  • 0x0000 000b:code_length = 11,说明接下来 11 个字节都存放的是字节码操作指令。

接下来的字节码指令串为 2a b7 00 01 2a 10 0a b5 00 02b1,每个字节都代表了一种字节码指令。

  • 2a:aload_0 从局部变量0中装载引用类型值入栈,这里是装入 this;
  • b7:invokespecial 调用操作数栈顶对象的实例构造器方法、private 方法或者它父类的方法,这里调用的是父类 Object 的方法。
    这个方法有两个字节说明具体调用的哪一个方法,紧随其后的是 0x00 01 是常量池索引 #1 的 <init> 方法。
  • 2a:aload_0 从局部变量0中装载引用类型值入栈;
  • 10:valuebyte值带符号扩展成int值入栈。
  • 0a:1(long)值入栈。
  • b5:给对象字段赋值。两个字节的参数表示给哪个对象赋值,也就是后面的 0x0002,查常量池是为参数 i 赋值。
  • b1:返回 void。

字节码指令本文不再深入,继续看后面的字节码:

  • 0x0000 :exception_table_length = 0,说明该方法不存在异常信息表;
  • 0x0002 :attributes_count=2,说明 Code 属性表内部还有两个表。

接下来的内容是第一个表 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
  • 0x000a:第一个表名,常量池 #10 为 LineNumberTable,这个表用于描述 Java 源码行号与字节码(偏移量)之间的对应关系。
  • 0x0000000a:属性长度为 10;
  • 0x0002:LineNumberTable 表长度为 2;

再往后面记录的是 LineNumberTable 中 line_number_info 的信息。line_number_info 是由两个 u2 类型数据组成的,前面 u1 表示字节码行号、后面 u1 表示代码行号。
我们知道 LineNumberTable 的长度为 2,line_number_info 应该有两对 u2 数据:

  • 0x0000:start_pc 字节码行号 0;
  • 0x0009:line_number 行号 9;
  • 0x0004:start_pc 字节码行号 4;
  • 0x000b:line_number 行号 11;

再往后是第二个表 "LocalVariableTable",这个表用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量直接的关系:

  • 0x000b:常量池 #11,指向 “LocalVariableTable” 字符串;
  • 0x0000000c:属性长度为 12;
  • 0x0001:局部变量信息表长度为 1;
  • 0x0000 : start_pc = 0 该局部变量字节码偏移量;
  • 0x000b:length = 11,该变量作用的长度 ;
  • 0x000c : name_index= "this",局部变量名称;
  • 0x000d : descriptor_index #13 ("Lcom/sky/test/Test"),局部变量描述符;
  • 0000 index=0,局部变量在栈帧局部变量表 Slot 的位置。

到此第一个方法以及描述完毕了,这个方法大概就是调用了父类 Object 的 init 方法,用来初始化变量等一系列操作。

鉴于篇幅第二个方法本文就不再分析,猜想也知是 Test.class 中的 test 方法,这块内容有一个了解即可。

Attribute

再往后就是 Attribute 了,Attribute 包含 "SourceFile" 表示 Class 文件的名称。通过工具查询是 #16 变量,名称为 Test.java。

总结

以上就是本文的全部内容了,感谢阅读。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345