【Java】从头到尾带你解读字节码,就是这么硬核!|Java 开发实战

858 阅读19分钟

这是我参与更文挑战的第7天,活动详情查看:更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

硬核游戏.gif

硬核警告!

一、什么是字节码

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.查看字节码的方式

  1. 首先打开idea,在里面创建一个.java文件
package test;

public class ByteCodeTest {
    private int a = 0;
    public int get() {
        return a;
    }
}

然后在另一个类上,运行main方法,调用这个类

  1. 找到编译后的.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字节码的格式

类型数量名称含义
u41magic魔数
u21minor_version副版本号
u21major_version主版本号
u21constant_pool_count常量数
cp_infoconstant_pool_count-1constant_pool常量池列表
u21access_flags访问标记
u21this_class当前类
u21super_class父类
u21interfaces_count实现的接口数
u2interfaces_countinterfaces接口列表
u21fields_count字段个数
field_infofields_countfields字段列表
u21methods_count方法个数
method_infomethods_countmethods方法列表
u21attribute_count属性个数
attribute_infoattributes_vountattributes属性列表

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)描述
PUBLIC0x00010000000000000001对应public类型的类
PRIVATE0x00020000000000000010字段为private
PROTECTED0x00040000000000000100字段为protected
STATIC0x00080000000000001000字段为static
FINAL0x00100000000000010000对应类的final声明
SUPER0x00200000000000100000标识JVM的invokespecial新语义
VOLATILE0x00400000000000100000字段是否为volatile
TRANSIENT0x00800000000001000000字段是否为transient
INTERFACE0x02000000001000000000接口标志
ABSTRACT0x04000000010000000000抽象类标志
SYNTHETIC0x10000001000000000000标识这个类并非用户代码产生
ANNOTATION0x20000010000000000000标识这是一个注解
ENUM0x40000100000000000000标识这是一个枚举
访问标记是根据每个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)

字段类型
表示字符含义
Bbyte字节类型
Jlong长整型
Cchar字符类型
Sshort短整型
Ddouble双精度浮点
Zboolean布尔型
Ffloat单精度浮点
Vvoid类型
Iint整型
L对象引用类型
字段
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道高频校招面试题,以及各种学习资料。