这是我参与更文挑战的第7天,活动详情查看:更文挑战
本文正在参加「Java主题月 – Java 开发实战」,详情查看 活动链接
硬核警告!
一、什么是字节码
1. 先来说一下什么是机械码
机械码就是cpu能够直接读取并运行的代码,它是用二进制编码表示的,也叫做机械指令码。在编写这种代码时,需要主动地去控制cpu的一切资源,而且需要记住全部指令所做的动作,十分的麻烦,当然这也是计算机的底层代码,处理开发计算机的专业人员之外,已经很少人去研究了。
2.字节码
字节码是一种中间状态的二进制文件,是由源码编译过来的,可读性没有源码的高。cpu并不能直接读取字节码,在java中,字节码需要经过JVM转译成机械码之后,cpu才能读取并运行。
3.使用字节码的好处
一处编译,到处运行。java就是典型的使用字节码作为中间语言,在一个地方编译了源码,拿着.class文件就可以在各种计算机运行,每个计算机上的jvm就会有所不同了。
4.字节码在JVM中的状态

5.额外提一点
编译型语言
只需要编译一次,就能够将源代码编译成机械码。执行效率高,可移植性低,依赖编译器。
典型代表:C、C++、Pascal、Object-C以及最近很火的苹果新语言swift,GO
解释型语言
在第一次编译时,并不会直接将源代码编译成机械码,而是编译成一种中间状态的二进制文件(字节码),由虚拟机来对这个二进制文件进行第二次编译,这次才是编译成机械码。执行效率比编译型语言低,但是可移植性高,依赖虚拟机。
典型代表:JavaScript、Python、Erlang、PHP、Perl、Ruby
二、java中的字节码
1.查看字节码的方式
- 首先打开idea,在里面创建一个.java文件
package test;
public class ByteCodeTest {
private int a = 0;
public int get() {
return a;
}
}
复制代码
然后在另一个类上,运行main方法,调用这个类
- 找到编译后的.class文件
out文件夹下面会多出一个我们刚刚编写的java文件相同名称的.class文件
3. 下载一个Sublime Text,然后打开.class文件
cafe babe 0000 0034 0016 0a00 0400 1209
0003 0013 0700 1407 0015 0100 0161 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 134c 7465 7374 2f42
7974 6543 6f64 6554 6573 743b 0100 0367
6574 0100 0328 2949 0100 0a53 6f75 7263
6546 696c 6501 0011 4279 7465 436f 6465
5465 7374 2e6a 6176 610c 0007 0008 0c00
0500 0601 0011 7465 7374 2f42 7974 6543
6f64 6554 6573 7401 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0021 0003 0004
0000 0001 0002 0005 0006 0000 0002 0001
0007 0008 0001 0009 0000 0038 0002 0001
0000 000a 2ab7 0001 2a03 b500 02b1 0000
0002 000a 0000 000a 0002 0000 0003 0004
0004 000b 0000 000c 0001 0000 000a 000c
000d 0000 0001 000e 000f 0001 0009 0000
002f 0001 0001 0000 0005 2ab4 0002 ac00
0000 0200 0a00 0000 0600 0100 0000 0600
0b00 0000 0c00 0100 0000 0500 0c00 0d00
0000 0100 1000 0000 0200 11
复制代码
2.一个疑惑
我也希望有大佬能够解答一下我疑惑,我去查百度也找不到答案,可能是我搜索方式有问题。
上面是.class文件的十六进制形式
在idea中有这样一个功能,view->Show ByteCode
用这个功能打开的是
// class version 52.0 (52)
// access flags 0x21
public class test/ByteCodeTest {
// compiled from: ByteCodeTest.java
// access flags 0x2
private I a
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 4 L1
ALOAD 0
ICONST_0
PUTFIELD test/ByteCodeTest.a : I
RETURN
L2
LOCALVARIABLE this Ltest/ByteCodeTest; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x1
public get()I
L0
LINENUMBER 6 L0
ALOAD 0
GETFIELD test/ByteCodeTest.a : I
IRETURN
L1
LOCALVARIABLE this Ltest/ByteCodeTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}
复制代码
我想知道这个跟十六进制文件的区别是什么?
它们俩是怎么转换的?
三、java字节码的组成
1.基本数据类型
数据类型 | 含义 |
---|---|
u1 | 无符号单字节整数 |
u2 | 无符号2字节整数 |
u4 | 无符号4字节整数 |
u8 | 无符号8字节整数 |
1Byte=8bit,在十六进制中,需要用两位数来表示1Byte。
一个十六进制数需要4bit来表示。
2.java字节码的格式
类型 | 数量 | 名称 | 含义 |
---|---|---|---|
u4 | 1 | magic | 魔数 |
u2 | 1 | minor_version | 副版本号 |
u2 | 1 | major_version | 主版本号 |
u2 | 1 | constant_pool_count | 常量数 |
cp_info | constant_pool_count-1 | constant_pool | 常量池列表 |
u2 | 1 | access_flags | 访问标记 |
u2 | 1 | this_class | 当前类 |
u2 | 1 | super_class | 父类 |
u2 | 1 | interfaces_count | 实现的接口数 |
u2 | interfaces_count | interfaces | 接口列表 |
u2 | 1 | fields_count | 字段个数 |
field_info | fields_count | fields | 字段列表 |
u2 | 1 | methods_count | 方法个数 |
method_info | methods_count | methods | 方法列表 |
u2 | 1 | attribute_count | 属性个数 |
attribute_info | attributes_vount | attributes | 属性列表 |
3.格式解读
为了节省空间,java对字节码的格式有严格要求,所以我们能够照着这个格式表来对字节码进行解读。
非基础数据类型的类型其实也是有基础数据类型来组成的,也是严格按照一定的格式来存放数据的。
可以看到常量池、接口、字段、方法、属性都是采用数量+数据的格式进行存储的。
四、解读字节码
以上面我们创建的ByteCodeTest.class文件为例。
1.魔数(magic)
cafe babe
复制代码
这个数是用来表示当前文件类型的,这个是由java之父James Gosling设定的。在代码内部也有魔数,一般被叫做魔法值,一般是指在方法内部的常量值。
2.版本号(version)
0000 0034
复制代码
副版本为0,主版本为52
对应java1.8(8),这个需要根据主版本跟副版本去查询。
3.常量池(constant_pool)
常量池中存储的是不会发生变化的数据。
常量池基本类型
常量个数(constant_pool_count)
0016
复制代码
0x16=22
这里指定了常量的个数,常量的个数为22,#0~#22,实际个数为21
为什么要减一我也不是很懂,有的说是因为#0不作为常量,有的说#0表示什么都不引用。
常量池列表(pool_count)
在观察常量时,需要先根据开头的一个字节判断它是什么类型,然后才能知道它的长度。
#1
0a00 0400 12
复制代码
0x0a=10,对应地找到了CONSTANT_Methodref_info
这个类型会引用两个u2(2bit),也就是8位16进制
所以这里是10个十六进制数表示一个常量
0x0004=4
0x0012=12
所以这个常量引用了#4、#12
全部常量
0a00 0400 12
09 0003 0013
0700 14
07 0015
0100 0161
0100 0149
0100 063c 696e 6974 3e
01 0003 2829 56
01 0004 436f 6465
0100 0f4c 696e 654e 756d 6265 7254 6162 6c65
0100 124c 6f63 616c 5661 7269 6162 6c65 5461 626c 65
01 0004 7468 6973
0100 134c 7465 7374 2f42 7974 6543 6f64 6554 6573 743b
0100 0367 6574
0100 0328 2949
0100 0a53 6f75 7263 6546 696c 65
01 0011 4279 7465 436f 6465 5465 7374 2e6a 6176 61
0c 0007 0008
0c00 0500 06
01 0011 7465 7374 2f42 7974 6543 6f64 6554 6573 74
01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374
复制代码
每一行都代表一个常量
类型为CONSTANT_UTF-8_info还需要另查ACSII码表
4.访问标记(access_flags)
访问标记
标记类型
标志名称 | 值(16进制) | 位(bit) | 描述 |
---|---|---|---|
PUBLIC | 0x0001 | 0000000000000001 | 对应public类型的类 |
PRIVATE | 0x0002 | 0000000000000010 | 字段为private |
PROTECTED | 0x0004 | 0000000000000100 | 字段为protected |
STATIC | 0x0008 | 0000000000001000 | 字段为static |
FINAL | 0x0010 | 0000000000010000 | 对应类的final声明 |
SUPER | 0x0020 | 0000000000100000 | 标识JVM的invokespecial新语义 |
VOLATILE | 0x0040 | 0000000000100000 | 字段是否为volatile |
TRANSIENT | 0x0080 | 0000000001000000 | 字段是否为transient |
INTERFACE | 0x0200 | 0000001000000000 | 接口标志 |
ABSTRACT | 0x0400 | 0000010000000000 | 抽象类标志 |
SYNTHETIC | 0x1000 | 0001000000000000 | 标识这个类并非用户代码产生 |
ANNOTATION | 0x2000 | 0010000000000000 | 标识这是一个注解 |
ENUM | 0x4000 | 0100000000000000 | 标识这是一个枚举 |
访问标记是根据每个bit上的0/1来标记的,从表中可以看出它是以16bit来表示的。 |
0021
复制代码
访问标记并不是直接对着表找,就可以找到是属于那个类型的。
0x0021=0000000000100001,可以对照表格找到,它在PUBLIC和SUPER上,所以这个类具有public和super标志
*[0000000000100001]:二进制
5.当前类(this_class)
当前类
表示指定在常量池的位置
0003
复制代码
0x0003=3
说明当前类对应#3,也就是
#3
0700 14
复制代码
这个又指向#20
#20
01 0011 7465 7374 2f42 7974 6543 6f64 6554 6573 74
acsii码表查询结果:test/ByteCodeTest
复制代码
6.父类(super_class)
当前类的父类
表示指定在常量池的位置
0004
复制代码
0x0004=4
#4
07 0015
复制代码
这个又指向#21
#21
01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374
acsii码表查询结果:java/lang/Object
复制代码
这里可以看出,这个类继承了Object类,所有类都继承这个类,所以我们没写,它也继承了。
7.接口(interfaces)
当前类实现的接口
接口数量(interfaces_count)
0000
复制代码
我这里没有实现任何接口,当然接口数量为0啦。
接口列表(interfaces)
如果有接口的话,后面会接interfaces_count* 4位16进制数,每个u2对应这常量池中的位置
8.字段(fields)
字段是指当前类的属性,不是方法内部的属性
字段个数(fields_count)
0001
复制代码
说明这个类有一个属性
然后我们读取后面的16位16进制数
字段列表(fields)
字段类型
表示字符 | 含义 |
---|---|
B | byte字节类型 |
J | long长整型 |
C | char字符类型 |
S | short短整型 |
D | double双精度浮点 |
Z | boolean布尔型 |
F | float单精度浮点 |
V | void类型 |
I | int整型 |
L | 对象引用类型 |
##### 字段 | |
“`java | |
0002 0005 0006 0000 | |
“` | |
第一个u2:字段的标记类型,标记类型,需要翻看前面的标记类型 | |
第二个u2:字段的名称,对应这常量池中的位置 | |
第三个u2:字段的类型,对应这常量池中的位置,需要翻看字段的类型 | |
第四个u2:字段的属性,对应这常量池中的位置 |
0002说明这个字段为private类型
0005指向常量池#5
#5
0100 0161
acsii码表查询结果:a
复制代码
0006指向常量池#6
#6
0100 0149
acsii码表查询结果:I
复制代码
I对应着int整型
0000指向#0,表示不作索引,也就是为null
如果在定义该属性时有赋值(int类型0是默认值),这个u2会指向一个不为null的常量
拼起来就是private int a;
9.方法(methods)
当前类的方法
方法个数(methods_count)
0002
复制代码
有两个方法,然而我们只定义了一个方法,那另外一个方法是哪里来的呢?
我们可以直接用idea打开编译好的.class文件,就可以看到,另外一个方法是构造方法
方法(methods)
我们先往后读6*4位16进制数
方法的描述
0001 0007 0008 0001
复制代码
第一个u2:方法的标记类型,标记类型,需要翻看前面的标记类型
第二个u2:方法的名称,对应这常量池中的位置
第三个u2:方法的类型,对应这常量池中的位置,需要翻看字段的类型
第四个u2:方法的属性个数
翻译过来就是 public ()V
有一个属性
方法的属性
我们需要往后读3*4位16进制数,这几位数说明了该方法的属性情况
0009 0000 0038
复制代码
第一个u2:属性的名称,对应这常量池中的位置
第二个u4:属性描述的长度,表示后面的u2个数,都是对属性的描述
第一个u2指向#9
#9
01 0004 436f 6465
acsii码表查询结果:Code
复制代码
这个Code是JVM虚拟机已经预定义好的属性,相当于方法内部的代码,详情去百度搜一下“JVM虚拟机规范预定义的属性”,这里我就不展开讲述了
第二个u2:0x38=56
那我们再往后读56*2位16进制数
0002 0001 0000 000a 2ab7 0001 2a03
b500 02b1 0000 0002 000a 0000 000a
0002 0000 0003 0004 0004 000b 0000
000c 0001 0000 000a 000c 000d 0000
复制代码
Code的属性结构
第一个u2:属性的最大堆数
第二个u2:属性的最大本地内存
第三个u4:指令描述的长度,表示后面的u2个数
第四个n*u2:指令,需要参照JVM 虚拟机字节码指令表
第五个u2:异常处理
第六个u2:属性的属性个数
·······后面就是属性的描述了
属性的解读跟前面的属性解读一样,但是需要注意的是,这些属性一般都是JVM虚拟机已经预定义好的属性,所以要按照相应的属性结构进行解读。
这里我就不解读了,
10.类属性
这个就是当前类的属性了
最后的几位16进制数就是对类属性的描述了
属性的个数
00 01
复制代码
表示有一个属性
属性的描述
00 1000 0000 0200 11
复制代码
第一个u2:属性常量的索引,对应这常量池中的位置
第二个u4:属性描述的长度,表示后面的u2个数
n*u2:对应这常量池中的位置
五、总结
.class文件的结构
魔数
cafe babe
版本号
0000 0034
常量池
0016
0a00 0400 12
09 0003 0013
0700 14
07 0015
0100 0161
0100 0149
0100 063c 696e 6974 3e
01 0003 2829 56
01 0004 436f 6465
0100 0f4c 696e 654e 756d 6265 7254 6162 6c65
0100 124c 6f63 616c 5661 7269 6162 6c65 5461 626c 65
01 0004 7468 6973
0100 134c 7465 7374 2f42 7974 6543 6f64 6554 6573 743b
0100 0367 6574
0100 0328 2949
0100 0a53 6f75 7263 6546 696c 65
01 0011 4279 7465 436f 6465 5465 7374 2e6a 6176 61
0c 0007 0008
0c00 0500 06
01 0011 7465 7374 2f42 7974 6543 6f64 6554 6573 74
01 0010 6a61 7661 2f6c616e 672f 4f62 6a65 6374
当前类的访问标记
0021
当前类
0003
父类
0004
实现接口数
0000
字段
0001
0002 0005 0006 0000
方法
方法个数
0002
方法描述
0001 0007 0008 0001
方法属性描述
0009 0000 0038
0002 0001 0000 000a 2ab7 0001 2a03
b500 02b1 0000 0002 000a 0000 000a
0002 0000 0003 0004 0004 000b 0000
000c 0001 0000 000a 000c 000d 0000
0001 000e 000f 0001
0009 0000 002f
0001 0001 0000 0005 2ab4 0002 ac00
0000 0200 0a00 0000 0600 0100 0000
0600 0b00 0000 0c00 0100 0000 0500
0c00 0d00 00
类属性
00 0100 1000 0000 0200 11
复制代码
启示
通过这次字节码的学习,我了解到了字节码的组成,java源代码是怎么编译成.class文件的。
但是这个可真难啊,什么都是规定死了的,只要对着结构表就可以解读了。
——————————————————————————————
你知道的越多,不知道的就越多。
如果本文章内容有问题,请直接评论或者私信我。如果觉得我写得还不错的话,点个赞也是对我的支持哦
未经允许,不得转载!
微信搜【程序员徐小白】,关注即可第一时间阅读最新文章。回复【面试题】有我准备的50道高频校招面试题,以及各种学习资料。