- 1、字节码查看方式
- 1.1、javap
- 1.2、jclasslib
- 2、字节码解析
- 2.1、编译前代码
- 2.2、编译后
- 2.3、字节码结构
- 2.4、简化理解字节码结构
- 2.4.1、Class文件结构
- 2.4.2、Class 摘要
- 2.4.3、常量池
- 2.4.4、方法栈帧
- 2.4.4.1、栈帧摘要
- 2.4.4.2、局部变量表
- 2.4.4.3、操作数栈
- 3、总结
.class 文件本身是二进制字节码,直接看的话太晦涩难懂,我们这边看的时候借助一些反汇编工具来查看。
1.1、javapjavap 可以反编译字节码文件。通过 javap -help 命令可以了解 javap 的基本用法。
D:workspaceDemoTestoutproductionDemoTestcomleotest>javap -help 用法: javap1.2、jclasslib其中, 可能的选项包括: -help --help -? 输出此用法消息 -version 版本信息 -v -verbose 输出附加信息 -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的 系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示最终常量 -classpath 指定查找用户类文件的位置 -cp 指定查找用户类文件的位置 -bootclasspath 覆盖引导类文件的位置
jclasslib Bytecode Viewer 是 IDEA 开发工具中的一个插件,可以方便查看每个 java 类编译后的字节码文件。具体安装方法很多,不再赘述。
package com.leo.test;
public class ByteCodeTest {
public static int num = 1;
public static int add(byte a, short b, int c, String d, float e, double f) {
int d_int = Integer.parseInt(d);
return (int) (a + b + c + d_int + e + f);
}
public static void main(String[] args) {
byte a = (byte) 0xFF; // -1
short b = 2;
int c = 3;
String d = "4";
float e = 5.0f;
double f = 6.0d;
boolean bool = false;
int g = add(a, b, c, d, e, f);
System.out.println(num); // 1
System.out.println(g); // 19
}
}
2.2、编译后
使用 javap 命令查看字节码:
javap -c -verbose ByteCodeTest.class
使用 javap 反编译后的文件内容如下:
Classfile /D:/workspace/DemoTest/out/production/DemoTest/com/leo/test/ByteCodeTest.class Last modified 2022-6-28; size 1136 bytes MD5 checksum 6550d8d70ee59d88e1ec1614d1b6f1e1 Compiled from "ByteCodeTest.java" public class com.leo.test.ByteCodeTest minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #12.#46 // java/lang/Object."2.3、字节码结构":()V #2 = Methodref #47.#48 // java/lang/Integer.parseInt:(Ljava/lang/String;)I #3 = String #49 // 4 #4 = Float 5.0f #5 = Double 6.0d #7 = Methodref #11.#50 // com/leo/test/ByteCodeTest.add:(BSILjava/lang/String;FD)I #8 = Fieldref #51.#52 // java/lang/System.out:Ljava/io/PrintStream; #9 = Fieldref #11.#53 // com/leo/test/ByteCodeTest.num:I #10 = Methodref #54.#55 // java/io/PrintStream.println:(I)V #11 = Class #56 // com/leo/test/ByteCodeTest #12 = Class #57 // java/lang/Object #13 = Utf8 num #14 = Utf8 I #15 = Utf8 #16 = Utf8 ()V #17 = Utf8 Code #18 = Utf8 LineNumberTable #19 = Utf8 LocalVariableTable #20 = Utf8 this #21 = Utf8 Lcom/leo/test/ByteCodeTest; #22 = Utf8 add #23 = Utf8 (BSILjava/lang/String;FD)I #24 = Utf8 a #25 = Utf8 B #26 = Utf8 b #27 = Utf8 S #28 = Utf8 c #29 = Utf8 d #30 = Utf8 Ljava/lang/String; #31 = Utf8 e #32 = Utf8 F #33 = Utf8 f #34 = Utf8 D #35 = Utf8 d_int #36 = Utf8 main #37 = Utf8 ([Ljava/lang/String;)V #38 = Utf8 args #39 = Utf8 [Ljava/lang/String; #40 = Utf8 bool #41 = Utf8 Z #42 = Utf8 g #43 = Utf8 #44 = Utf8 SourceFile #45 = Utf8 ByteCodeTest.java #46 = NameAndType #15:#16 // " ":()V #47 = Class #58 // java/lang/Integer #48 = NameAndType #59:#60 // parseInt:(Ljava/lang/String;)I #49 = Utf8 4 #50 = NameAndType #22:#23 // add:(BSILjava/lang/String;FD)I #51 = Class #61 // java/lang/System #52 = NameAndType #62:#63 // out:Ljava/io/PrintStream; #53 = NameAndType #13:#14 // num:I #54 = Class #64 // java/io/PrintStream #55 = NameAndType #65:#66 // println:(I)V #56 = Utf8 com/leo/test/ByteCodeTest #57 = Utf8 java/lang/Object #58 = Utf8 java/lang/Integer #59 = Utf8 parseInt #60 = Utf8 (Ljava/lang/String;)I #61 = Utf8 java/lang/System #62 = Utf8 out #63 = Utf8 Ljava/io/PrintStream; #64 = Utf8 java/io/PrintStream #65 = Utf8 println #66 = Utf8 (I)V { public static int num; descriptor: I flags: ACC_PUBLIC, ACC_STATIC public com.leo.test.ByteCodeTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/leo/test/ByteCodeTest; public static int add(byte, short, int, java.lang.String, float, double); descriptor: (BSILjava/lang/String;FD)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=8, args_size=6 0: aload_3 1: invokestatic #2 // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I 4: istore 7 6: iload_0 7: iload_1 8: iadd 9: iload_2 10: iadd 11: iload 7 13: iadd 14: i2f 15: fload 4 17: fadd 18: f2d 19: dload 5 21: dadd 22: d2i 23: ireturn LineNumberTable: line 7: 0 line 8: 6 LocalVariableTable: Start Length Slot Name Signature 0 24 0 a B 0 24 1 b S 0 24 2 c I 0 24 3 d Ljava/lang/String; 0 24 4 e F 0 24 5 f D 6 18 7 d_int I public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=7, locals=10, args_size=1 0: iconst_m1 1: istore_1 2: iconst_2 3: istore_2 4: iconst_3 5: istore_3 6: ldc #3 // String 4 8: astore 4 10: ldc #4 // float 5.0f 12: fstore 5 14: ldc2_w #5 // double 6.0d 17: dstore 6 19: iconst_0 20: istore 8 22: iload_1 23: iload_2 24: iload_3 25: aload 4 27: fload 5 29: dload 6 31: invokestatic #7 // Method add:(BSILjava/lang/String;FD)I 34: istore 9 36: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 39: getstatic #9 // Field num:I 42: invokevirtual #10 // Method java/io/PrintStream.println:(I)V 45: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 48: iload 9 50: invokevirtual #10 // Method java/io/PrintStream.println:(I)V 53: return LineNumberTable: line 12: 0 line 13: 2 line 14: 4 line 15: 6 line 16: 10 line 17: 14 line 18: 19 line 19: 22 line 20: 36 line 21: 45 line 22: 53 LocalVariableTable: Start Length Slot Name Signature 0 54 0 args [Ljava/lang/String; 2 52 1 a B 4 50 2 b S 6 48 3 c I 10 44 4 d Ljava/lang/String; 14 40 5 e F 19 35 6 f D 22 32 8 bool Z 36 18 9 g I static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: iconst_1 1: putstatic #9 // Field num:I 4: return LineNumberTable: line 4: 0 } SourceFile: "ByteCodeTest.java"
引用 Oracle 官方给的字节码结构如下:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
解释一下
| 类型 | 名称 | 解释 | 长度 | 数量 |
|---|---|---|---|---|
| u4 | magic | 魔数 | 4个字节 | 1 |
| u2 | minor_version | 次版本号 | 2个字节 | 1 |
| u2 | major_version | 主版本号 | 2个字节 | 1 |
| u2 | constant_pool_count | 常量池大小 | 2个字节 | 1 |
| cp_info | constant_pool[] | 常量池 | n个字节 | costant_pool_count - 1 |
| u2 | access_flags | 类的访问控制权限 | 2个字节 | 1 |
| u2 | this_class | 类索引 | 2个字节 | 1 |
| u2 | super_class | 父类索引 | 2个字节 | 1 |
| u2 | interfaces_count | 接口索引数量 | 2个字节 | 1 |
| u2 | interfaces[] | 实现的接口内容 | 2个字节 | interfaces_count |
| u2 | fields_count | 成员属性数量 | 2个字节 | 1 |
| field_info | fields[] | 成员属性值 | n个字节 | fields_count |
| u2 | methods_count | 方法数量 | 2个字节 | 1 |
| method_info | methods[] | 方法表 | n个字节 | methods_count |
| u2 | attributes_count | 类属性数量 | 2个字节 | 1 |
| attribute_info | attributes[] | 类属性值 | n个字节 | attributes_count |
-
无符号数属于最基本的数据类型。它以 u1、u2、u4、u8 分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值。例如下表中第一行中的 u4 表示 Class 文件前 4 个字节表示该文件的魔数,第二行的 u2 表示该 Class 文件第 5-6 个字节表示该 JDK 的次版本号。
-
表是由多个无符号数或者其他表作为数据项构成的复合数据类型。所有表都习惯性地以_info结尾。表用于描述有层次关系的复合结构的数据,例如下表第 5 行表示其实一个类型为 cp_info 的表(常量池),这里面存储了该类的所有常量。
借用一张 Class文件字节码结构组织示意图:
典型的class文件分为:MagicNumber,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods 和Attributes这十个部分。
但是为了方便理解,借用简化后的模型:Class摘要、常量池、方法栈帧
摘要主要就是记录class的一些基本信息,包括类的大小、修改时间、校验和、JDK版本信息、类的访问范围等。
D:workspaceDemoTestoutproductionDemoTestcomleotest>javap -v ByteCodeTest.class // 反编译字节码文件 ByteCodeTest.class Classfile /D:/workspace/DemoTest/out/production/DemoTest/com/leo/test/ByteCodeTest.class // 文件来源,可以看到文件的路径 Last modified 2022-6-28; size 1136 bytes // 类最后修改时间、类大小 MD5 checksum 6550d8d70ee59d88e1ec1614d1b6f1e1 // 校验和 Compiled from "ByteCodeTest.java" // 编译的文件 public class com.leo.test.ByteCodeTest // 全类名 minor version: 0 // jdk 子版本号 major version: 52 // jdk 主版本号,52 代表的就是 jdk1.8 flags: ACC_PUBLIC, ACC_SUPER // 说明这个类是 public 类,初始化方法使用父类的2.4.3、常量池
- 字面量:包括文本字符串,final修饰的成员变量,还有数据的值等,对于基本数值int类型的常量,常量池值保存了引用和字面名称,没有保存数据的值
#3 = String #49 // 4,引用的是 #49 行的值 #4 = Float 5.0f // float 类型 5.0 #5 = Double 6.0d // double 类型 6.0 #28 = Utf8 c // int 类型只保存了名称,没有存实际值 ......中间省略了 #49 = Utf8 4
- 符号引用:类、接口的名称和描述
#7 = Methodref #11.#50 // com/leo/test/ByteCodeTest.add:(BSILjava/lang/String;FD)I #8 = Fieldref #51.#52 // java/lang/System.out:Ljava/io/PrintStream; #9 = Fieldref #11.#53 // com/leo/test/ByteCodeTest.num:I #10 = Methodref #54.#55 // java/io/PrintStream.println:(I)V #11 = Class #56 // com/leo/test/ByteCodeTest #12 = Class #57 // java/lang/Object #57 = Utf8 java/lang/Object ......中间省略了 #50 = NameAndType #22:#23 // add:(BSILjava/lang/String;FD)I #51 = Class #61 // java/lang/System #52 = NameAndType #62:#63 // out:Ljava/io/PrintStream; #53 = NameAndType #13:#14 // num:I #54 = Class #64 // java/io/PrintStream #55 = NameAndType #65:#66 // println:(I)V2.4.4、方法栈帧
代码样例里面给的两个方法 add 和 main 方法:
public static int add(byte, short, int, java.lang.String, float, double);
descriptor: (BSILjava/lang/String;FD)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=8, args_size=6
0: aload_3
1: invokestatic #2 // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I
4: istore 7
6: iload_0
7: iload_1
8: iadd
9: iload_2
10: iadd
11: iload 7
13: iadd
14: i2f
15: fload 4
17: fadd
18: f2d
19: dload 5
21: dadd
22: d2i
23: ireturn
LineNumberTable:
line 7: 0
line 8: 6
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 a B
0 24 1 b S
0 24 2 c I
0 24 3 d Ljava/lang/String;
0 24 4 e F
0 24 5 f D
6 18 7 d_int I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=7, locals=10, args_size=1
0: iconst_m1
1: istore_1
2: iconst_2
3: istore_2
4: iconst_3
5: istore_3
6: ldc #3 // String 4
8: astore 4
10: ldc #4 // float 5.0f
12: fstore 5
14: ldc2_w #5 // double 6.0d
17: dstore 6
19: iconst_0
20: istore 8
22: iload_1
23: iload_2
24: iload_3
25: aload 4
27: fload 5
29: dload 6
31: invokestatic #7 // Method add:(BSILjava/lang/String;FD)I
34: istore 9
36: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
39: getstatic #9 // Field num:I
42: invokevirtual #10 // Method java/io/PrintStream.println:(I)V
45: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
48: iload 9
50: invokevirtual #10 // Method java/io/PrintStream.println:(I)V
53: return
LineNumberTable:
line 12: 0
line 13: 2
line 14: 4
line 15: 6
line 16: 10
line 17: 14
line 18: 19
line 19: 22
line 20: 36
line 21: 45
line 22: 53
LocalVariableTable:
Start Length Slot Name Signature
0 54 0 args [Ljava/lang/String;
2 52 1 a B
4 50 2 b S
6 48 3 c I
10 44 4 d Ljava/lang/String;
14 40 5 e F
19 35 6 f D
22 32 8 bool Z
36 18 9 g I
2.4.4.1、栈帧摘要
// add 方法
public static int add(byte, short, int, java.lang.String, float, double);
descriptor: (BSILjava/lang/String;FD)I // 返回值 I 指的是 int 类型
flags: ACC_PUBLIC, ACC_STATIC // 公共方法、静态方法
Code:
stack=4, locals=8, args_size=6 // 栈帧深度是4,最大局部变量8个,入参个数6个
// main 方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V // 返回值 V 指的是 void 类型
flags: ACC_PUBLIC, ACC_STATIC // 公共方法、静态方法
Code:
stack=7, locals=10, args_size=1 // 栈帧深度是7,最大局部变量10个,入参个数1个
2.4.4.2、局部变量表
局部变量表是一个数组,用来存储当前方法的局部变量,表中可以存储的类型包括boolean、byte、char、short、int、float以及引用类型,因为局部变量是线程私有的,所以不会有线程安全问题,每个栈帧中本地变量表的大小,在.class字节码文件编译完成之后,就已经确定。
// add 方法
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 a B
0 24 1 b S
0 24 2 c I
0 24 3 d Ljava/lang/String;
0 24 4 e F
0 24 5 f D
6 18 7 d_int I
// main 方法
LocalVariableTable:
Start Length Slot Name Signature
0 54 0 args [Ljava/lang/String; // 入参是 args ,类型是 String 数组
2 52 1 a B
4 50 2 b S
6 48 3 c I
10 44 4 d Ljava/lang/String; // 局部变量,类型是 String
14 40 5 e F
19 35 6 f D
22 32 8 bool Z
36 18 9 g I
在 JVM 规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型,方法的参数列表(包括数量,类型与顺序) 与返回值,根据描述符规则,基本数据类型和代表无返回值的 void 类型都用一个大写的字符来表示的,对象类型则使用字符 L加对象的全限定名称来表示,为了压缩字节码文件的体积。对于基本的数据类型,JVM 都只使用一个大写的字母来表示,如下所示:
# main方法的操作数栈:
0: iconst_m1 // 将 int 类型常量 -1 压入栈, 这个就是 a 的值, byte 操作的时候会补齐高位按照 int 值操作
1: istore_1 // 将 int 类型值存入局部变量 1
2: iconst_2 // 将 int 类型常量 2 压入栈
3: istore_2 // 将 int 类型值存入局部变量 2
4: iconst_3 // 将 int 类型常量 3 压入栈
5: istore_3 // 将 int 类型值存入局部变量 3
6: ldc #3 // String 4, 将常量池中的项压入栈
8: astore 4 // 将引用类型值存入局部变量 4
10: ldc #4 // float 5.0f, 把常量池中的项压入栈
12: fstore 5 // 将 float 类型值存入局部变量 5
14: ldc2_w #5 // double 6.0d, 把常量池中的 double 类型的项压入栈(使用宽索引,long类型也是)
17: dstore 6 // 将 double 类型值存入局部变量 6
19: iconst_0 // 将 int 类型常量 0 压入栈,boolean 的值只有两个: true=1, false=0
20: istore 8 // 将 int 类型值存入局部变量 8
22: iload_1 // 从局部变量 1 中装载 int 类型值
23: iload_2 // 从局部变量 2 中装载 int 类型值
24: iload_3 // 从局部变量 3 中装载 int 类型值
25: aload 4 // 从局部变量 4 中装载 引用 类型值
27: fload 5 // 从局部变量 5 中装载 float 类型值
29: dload 6 // 从局部变量 6 中装载 double 类型值
31: invokestatic #7 // Method add:(BSILjava/lang/String;FD)I, 调用类静态方法 add
34: istore 9 // 将 int 类型值存入局部变量
36: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;, 从类中获取静态字段
39: getstatic #9 // Field num:I, 从类中获取静态字段 num 类型为 int
42: invokevirtual #10 // Method java/io/PrintStream.println:(I)V, 调度对象的实现方法
45: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;, 从类中获取静态字段
48: iload 9 // 从局部变量 9 中装载 int 类型值
50: invokevirtual #10 // Method java/io/PrintStream.println:(I)V,, 调度对象的实现方法
53: return // 从方法中返回,返回值为void
指令的分类:
- 根据指令的性质,主要可以分为四个类型:
- 栈操作指令,包括与局部变量交互的指令
- 程序流程控制指令
- 对象操作指令,包括方法调用指令
- 算术运算以及类型转换指令
常用指令:
- 【局部变量压栈指令】将一个局部变量加载到操作数栈:xload、xload_(其中x为i、l、f、d、a,n为0到3)
- 【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst、lconst、fconst、dconst
- 【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_(其中x为i、l、f、d、a,n为0到3);xastore(其中x为i、1、f、d、a、b、c、s)
上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload)。这些指令助记符实际上代表了一组指令(例如iload代表了iload0、iload1、iload2和iload3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。
iload_0:将局部变量表中索引为 0 位置上的数据压入操作数栈中。
iload 0:将局部变量表中索引为 0 位置上的数据压入操作数栈中。
iload 4:将局部变量表中索引为 4 位置上的数据压入操作数栈中。
前两种所表达的意思是相同的,不过iload_0相当于是只有操作码所以只占用1个字节,而iload 0 是操作码和操作数所组成的,而操作数占 2 个字节,所以占用3个字节。
更多指令请参考《JVM 汇编指令 栈和局部变量操作》
3、总结本文只是简单了解了字节码查看方式、字节码文件的结构、字节码怎么运行的,线程和栈帧之间的关系如下图(网图,帮助理解):
最终引用一下网上的线程和栈帧之间的调用过程:
线程创建->线程栈创建->调用栈帧A->栈帧A创建->栈帧A调用操作数栈->栈帧A操作数栈调用栈帧B->栈帧B创建->栈帧B调用操作数栈->栈帧B返回->栈帧B销毁->栈帧A返回->栈帧A销毁->线程栈销毁->线程销毁
- 线程创建
- 线程栈创建
- 调用栈帧A
- 栈帧A创建
- 栈帧A调用操作数栈
- 栈帧A操作数栈调用栈帧B
- 栈帧B创建
- 栈帧B调用操作数栈
- 栈帧B返回
- 栈帧B销毁
- 栈帧A返回
- 栈帧A销毁
- 线程栈销毁
- 线程销毁
文章引用
https://blog.csdn.net/xidianzxm/article/details/108634553(线程的运行原理-栈帧图解/多线程栈帧)
https://blog.csdn.net/Mr_YarNell/article/details/118482034(Java字节码(Java bytecode))



