文章目录
1、字节码查看方式
.class 文件本身是二进制字节码,直接看的话太晦涩难懂,我们这边看的时候借助一些反汇编工具来查看。
1.1、javap
javap
可以反编译字节码文件。通过 javap -help
命令可以了解 javap 的基本用法。
D:\workspace\DemoTest\out\production\DemoTest\com\leo\test>javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
1.2、jclasslib
jclasslib Bytecode Viewer
是 IDEA 开发工具中的一个插件,可以方便查看每个 java 类编译后的字节码文件。具体安装方法很多,不再赘述。
2、字节码解析
2.1、编译前代码
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."<init>":()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 <init>
#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 <clinit>
#44 = Utf8 SourceFile
#45 = Utf8 ByteCodeTest.java
#46 = NameAndType #15:#16 // "<init>":()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."<init>":()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"
2.3、字节码结构
引用 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文件字节码结构组织示意图:
2.4、简化理解字节码结构
2.4.1、Class文件结构
典型的class文件分为:MagicNumber,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods 和Attributes这十个部分。
但是为了方便理解,借用简化后的模型:Class摘要
、常量池
、方法栈帧
2.4.2、Class 摘要
摘要主要就是记录class的一些基本信息,包括类的大小、修改时间、校验和、JDK版本信息、类的访问范围等。
D:\workspace\DemoTest\out\production\DemoTest\com\leo\test>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)V
2.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 都只使用一个大写的字母来表示,如下所示:
2.4.4.3、操作数栈
# 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))
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72599.html