字节码
在 java
学习过程中,我们编写的基本都是 .java
结尾的文件,这样的文件称为源文件;是不能直接运行的。所以需要将 .java
文件转换为 .class
字节码文件,然后通过JVM
虚拟机将 .class
字节码文件,转化为对应系统可运行的机器指令,才能完成运行。
这也就是 java
语言一处编译,处处运行的原理。
(有 JVM
的存在; JVM
能够根据不同的操作系统,将 .class
文件编译成对应系统可支持运行的机器指令。)
字节码的结构
接下来我们会从字节码的结构、执行过程等方面对字节码进行剖析。
源文件
public class Demo {
public static void main(String[] args) {
int a = 1;
int b = 1;
int c = add(a, b);
System.out.println(c);
}
public static int add(int a, int b) {
return a + b;
}
}
字节码文件
使用javac
命令将源文件编译成字节码文件。
命令:javac -g:vars,lines xxx.java
,vars
与lines
表示额外编译本地变量表和代码行对照表(后续说明作用)
Demo.class
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 34 00 1F 0A 00 06 00 11 0A J~:>...4........
00000010: 00 05 00 12 09 00 13 00 14 0A 00 15 00 16 07 00 ................
00000020: 17 07 00 18 01 00 06 3C 69 6E 69 74 3E 01 00 03 .......<init>...
00000030: 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E ()V...Code...Lin
00000040: 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D eNumberTable...m
00000050: 61 69 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 ain...([Ljava/la
00000060: 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 03 61 ng/String;)V...a
00000070: 64 64 01 00 05 28 49 49 29 49 01 00 0A 53 6F 75 dd...(II)I...Sou
00000080: 72 63 65 46 69 6C 65 01 00 09 44 65 6D 6F 2E 6A rceFile...Demo.j
00000090: 61 76 61 0C 00 07 00 08 0C 00 0D 00 0E 07 00 19 ava.............
000000a0: 0C 00 1A 00 1B 07 00 1C 0C 00 1D 00 1E 01 00 04 ................
000000b0: 44 65 6D 6F 01 00 10 6A 61 76 61 2F 6C 61 6E 67 Demo...java/lang
000000c0: 2F 4F 62 6A 65 63 74 01 00 10 6A 61 76 61 2F 6C /Object...java/l
000000d0: 61 6E 67 2F 53 79 73 74 65 6D 01 00 03 6F 75 74 ang/System...out
000000e0: 01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E ...Ljava/io/Prin
000000f0: 74 53 74 72 65 61 6D 3B 01 00 13 6A 61 76 61 2F tStream;...java/
00000100: 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 01 00 io/PrintStream..
00000110: 07 70 72 69 6E 74 6C 6E 01 00 04 28 49 29 56 00 .println...(I)V.
00000120: 21 00 05 00 06 00 00 00 00 00 03 00 01 00 07 00 !...............
00000130: 08 00 01 00 09 00 00 00 1D 00 01 00 01 00 00 00 ................
00000140: 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06 .*7..1..........
00000150: 00 01 00 00 00 01 00 09 00 0B 00 0C 00 01 00 09 ................
00000160: 00 00 00 3A 00 02 00 04 00 00 00 12 04 3C 04 3D ...:.........<.=
00000170: 1B 1C B8 00 02 3E B2 00 03 1D B6 00 04 B1 00 00 ..8..>2...6..1..
00000180: 00 01 00 0A 00 00 00 16 00 05 00 00 00 04 00 02 ................
00000190: 00 05 00 04 00 06 00 0A 00 07 00 11 00 08 00 09 ................
000001a0: 00 0D 00 0E 00 01 00 09 00 00 00 1C 00 02 00 02 ................
000001b0: 00 00 00 04 1A 1B 60 AC 00 00 00 01 00 0A 00 00 ......`,........
000001c0: 00 06 00 01 00 00 00 0B 00 01 00 0F 00 00 00 02 ................
000001d0: 00 10
开头的四个字符CA FE BA BE
表示当前文件是一个字节码文件,JVM
会以文件是否以这四个字开头来判断为.class
文件,进而进行加载。
注意:
.class
文件直接使用记事本打开会乱码,这里推荐使用vs code
工具,安装hexdump for VSCode
插件,便能够正常打开字节码文件了。
javap
命令
当我们生成字节码文件后,看得懂吗?是不是一脸懵逼的打开,然后一脸懵逼的关上,心想这什么玩意儿?
不要着急,Java提供了工具命令,能够帮我们更好的观看字节码文件。
javap
命令介绍
javap
是jdk
自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
使用
命令:javap -v Demo.class
以下就是使用javap
命令生成好的汇编文件,大致可分为类基本信息、常量池、包含方法
Classfile /C:/Users/Yhf19/Desktop/demo/Demo.class
Last modified 2021-5-16; size 609 bytes
MD5 checksum e7474a62c3f9443a0c4b65c7de331a4d
public class Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = Methodref #5.#25 // Demo.add:(II)I
#3 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #28.#29 // java/io/PrintStream.println:(I)V
#5 = Class #30 // Demo
#6 = Class #31 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LDemo;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 a
#19 = Utf8 I
#20 = Utf8 b
#21 = Utf8 c
#22 = Utf8 add
#23 = Utf8 (II)I
#24 = NameAndType #7:#8 // "<init>":()V
#25 = NameAndType #22:#23 // add:(II)I
#26 = Class #32 // java/lang/System
#27 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#28 = Class #35 // java/io/PrintStream
#29 = NameAndType #36:#37 // println:(I)V
#30 = Utf8 Demo
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (I)V
{
public Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LDemo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: invokestatic #2 // Method add:(II)I
9: istore_3
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 4: 0
line 5: 2
line 6: 4
line 7: 10
line 8: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
2 16 1 a I
4 14 2 b I
10 8 3 c I
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 a I
0 4 1 b I
}
类基本信息
Classfile /C:/Users/Yhf19/Desktop/demo/Demo.class
Last modified 2021-5-16; size 609 bytes
MD5 checksum e7474a62c3f9443a0c4b65c7de331a4d
public class Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
前三行描述了.class
文件的所在路径、最后修改时间、校验和。
后三行描述了类的基本信息。
-
minor
:此版本号 -
major
:主版本号 -
flags
:描述了类的相关修饰符,ACC_PUBLIC
表示类是public
公开类型,flag
可以有以下取值
常量池
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = Methodref #5.#25 // Demo.add:(II)I
#3 = Fieldref #26.#27 //java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #28.#29 // java/io/PrintStream.println:(I)V
#5 = Class #30 // Demo
#6 = Class #31 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
这一部分就是类中使用到的常量,每个常量都有编号,以#
号开头,=
号后面就是常量类型,类型后就是常量的值。每个常量都由三部分组成——编号、类型、常量值组成。
例如#1
常量的类型是Methodref
,表示这个常量描述的是方法相关的信息;#5
变量的类型是Class
,表示这个常量描述的是类相关的信息。
注意:一个常量还可以引用其他常量,例如#2
就引用了#5
与#25
常量的值。而最终#2
得到的值就是
// Demo.add:(II)I
包含方法
{
public Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
// 省略
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
// 省略
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
// 省略
}
这一部分就是包含方法,也就是在当前类中调用了那些方法,都会在这里一一列举出来。
方法结构
我们以main()
方法为例,对方法中的结构进行了解。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: invokestatic #2 // Method add:(II)I
9: istore_3
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 4: 0
line 5: 2
line 6: 4
line 7: 10
line 8: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
2 16 1 a I
4 14 2 b I
10 8 3 c I
对于方法我们可以分为方法描述和Code这两部分。
方法描述
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
方法描述就是对方法签名,权限等一系列的描述。
-
descriptor
:精简的描述了方法的入参和出参。[
:一个"["表示一维数组;L
:为类描述符,表示Stirng
是一个引用类型。V
: 则表示方法的返回值为void
类型,也就是无返回值。
Code部分
code关键字以下的部分我们也可以进行划分为指令表、本地变量表、代码行对照表三部分。
指令表
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: invokestatic #2 // Method add:(II)I
9: istore_3
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
17: return
该部分第一行是该方法的指令以及执行过程的相关信息,这一行信息包括:
args_size
是参数数量,在主函数中,因为有args
这个参数,所以在这里args_size
为 1;locals
是该方法中的本地变量有多少个,在我们的主函数里面有定义了 3 个变量,加上一个参数,因此有 4 个变量;stack
是方法在执行过程中,操作数栈中最大深度,这个在之后讲解指令执行过程时可以看出。
在这一行信息之后是字节码指令,一条指令包括偏移量以及执行的指令码,PC Register 利用偏移量来判断指令执行位置。
代码行对照表
LineNumberTable:
line 4: 0
line 5: 2
line 6: 4
line 7: 10
line 8: 17
以line 4: 0
为例,表示源.java
文件的第四行代码,对应的是0偏移量
本地变量表
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
2 16 1 a I
4 14 2 b I
10 8 3 c I
-
start
:为这个变量可见的起始偏移位置,它的值必须是在 Code 中存在的偏移量值。(也就是这个变量第一次出现在哪个偏移量) -
length
:为该变量的有效长度,描述的是该变量在方法中的有效范围大小。在这个例子中,我们的变量直到方法末尾都有效,因此你会发现
start + lenth
的值都是 18 (方法中执行的指令数)。当我们在一个局部的代码块里面声明一个变量,那么它的有效期长度将会更短。 -
slot
:为变量在 local variable(本地变量表) 中的位置,这可以帮助我们在指令中确定对应的变量。 -
Name
则是变量名 -
Signature
为该变量的类型
当我们了解了class
文件的结构之后,我们就可以进行流程分析,但是在分析之前,我们还需要了解以下JVM的内存模型,能够更好的帮助我们进行分析。
JVM内存模型
我们都知道在Java存在多线程,当多个线程访问同一个资源的时候,我们就称这个资源为共享资源;当然也有每个线程独有的资源。
我们就根据资源的可用范围将JVM进行以下划分。
JVM内存模型
其中方法区和堆内存是资源共享的,所有线程都可以进行访问。
虚拟机栈、本地方法栈以及程序计数器是线程独有的;
- PC Register(程序计数器) 用于记录当前线程指令的执行位置。由于一个进程可能有多个线程,而 CPU 会在不同线程之间切换,为了能够记录各个线程的当前执行的指令,每个线程都需要有一个 PC Register,来保证各个线程都可以进行独立运算。
- JVM Stack(虚拟机栈) 用于存放调用方法时压入栈的栈帧。相信学过数据结构的对栈应该不陌生,JVM Stack 压入的单位为栈帧(Frame),用于存储数据、动态链接、方法返回值和调度异常等。每次调用一个方法都会创建一个新的栈帧压入 JVM Stack 来存储该方法的信息,当该方法调用完成时,对应的栈帧也会跟着被销毁。一个栈帧都有自己的局部变量数组、操作数栈、对当前方法类的运行常量池的引用。
- Native Method Stack(本地方法栈) 则是用于调用操作系统本地方法时使用的栈空间。
当我们调用一个方法的时候,就会创建一个新的栈帧,然后继续入栈(JVM stack),执行完成后栈帧销毁(也成为弹栈)。所以在栈帧中包含了哪些东西,才能够支撑栈帧的执行呢,我们来看看。
栈帧与JVM stack
每个栈帧中都存放了本地变量表、操作栈以及常量引用池
指令的执行过程
基本指令含义
接下来我们一步步执行方法中的指令,在这里我们先对出现的几个指令做一个简单的介绍:
iconst_<i>
放一个 int 常量放到 operand stack 中istore_<n>
从 operand stack 中获取一个 int 到 local variable 的 n 中iload_<n>
从 local variable 中读取 int 变量 n 的值到操作数栈中invokestatic
调用一个 class 的 static 方法getstatic
从 class 中获取一个 static 字段invokevirtual
调用一个实例方法,基于类的调度return
从方法中返回一个 void,ireturn
从方法中返回 operand stack 栈顶的 int
更多的指令与详细的说明请查看文章最后参考中的官方指令文档
这里要注意,操作数栈的数是被取出操作,被取出的数将不会继续在 operand stack 里面。
main()
方法指令执行分析
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
// 把int常量1放入操作栈
0: iconst_1
// 从操作栈中取出1常量,放入本地变量表Slot为1位置
1: istore_1
// 把int常量1放入操作栈
2: iconst_1
// 从操作栈中取出1常量,放入本地变量表Slot为2位置
3: istore_2
// 将常量1赋值给本地变量表Slot为1位置的变量a
4: iload_1
// // 将常量1赋值给本地变量表Slot为2位置的变量b
5: iload_2
// 调用静态方法add(),并传入(II)两个Int类型的参数,返回值为I int类型
6: invokestatic #2 // Method add:(II)I
// 将add()方法返回的数据由操作栈,存储到本地变量表Slot为3位置
9: istore_3
// 获取静态字段
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
// 将本地变量表Solt为3位置上的常量3赋值给对应位置变量c
13: iload_3
// 调用println方法,传入int变量,该方法返回值为Void
14: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
// main方法返回
17: return
LineNumberTable:
line 4: 0
line 5: 2
line 6: 4
line 7: 10
line 8: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
2 16 1 a I
4 14 2 b I
10 8 3 c I
总结:
- 在方法作为栈帧入栈的时候,本地变量表就已经生成了,每个Slot对应了哪个变量也已经就绪。
- 凡是生成的常量生成后都需由 operand stack 存储,再由指令
store
存储到 local variable Table中。
美团文章参考:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
InfoQ文章参考:https://xie.infoq.cn/article/a9fbc16488d3ebbe4b758ff92
指令参考:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html
入了皮毛,后续理解深入后在做修改。
当作初学者。