java字节码,就是jvm执行的一种指令格式.jvm通过字节码指令,做相对应的动作
字节码查看(助记符->二进制)
.class文件本身是二进制文件,我们可以以两种方式查看,一种就是直接看它的二进制内容,但是不方便查看和理解,还有一种是看它经过javap转成助记符模式的内容,方便我们理解
二进制查看方式
二进制文件查看器
因为电脑中的二进制文件,一般文本编辑器在打开的同时会给做一些转义,这里推荐使用比如winhex
这种软件查看,便于理解
助记符查看
javap
使用javac
命令可以将.java文件编译成.class文件,这个.class文件,就是所谓的java字节码文件.
使用javap -c
命令可以查看.class文件的字节码.
使用javap -c -verbose
命令可以查看字节码的详细信息.
IDEA 插件
jclasslib Bytecode Viewer 插件,可以方便查看每个java类编译后的字节码文件
简单的字节码解析
编译前java代码
package com.rrtx.adm;
/**
* Created by yarne on 2021/7/3.
*/
public class Main {
public static void main(String[] args) {
int a = 5;
double b = 2.00;
String c = "3";
add(c,a,b);
}
public static void add(String a,int b,double c){
double v = Integer.valueOf(a) + b + c;
System.out.println(v);
}
}
查看.class的字节码(助记符)
Classfile /C:/Users/14641/Desktop/Main.class
Last modified 2021-7-4; size 890 bytes
MD5 checksum 683b4cdcd60108ce5bfdcc9e6fe3c992
Compiled from "Main.java"
public class com.rrtx.adm.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#34 // java/lang/Object."<init>":()V
#2 = Double 2.0d
#4 = String #35 // 3
#5 = Methodref #10.#36 // com/rrtx/adm/Main.add:(Ljava/lang/String;ID)V
#6 = Methodref #37.#38 // java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
#7 = Methodref #37.#39 // java/lang/Integer.intValue:()I
#8 = Fieldref #40.#41 // java/lang/System.out:Ljava/io/PrintStream;
#9 = Methodref #42.#43 // java/io/PrintStream.println:(D)V
#10 = Class #44 // com/rrtx/adm/Main
#11 = Class #45 // java/lang/Object
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/rrtx/adm/Main;
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 a
#24 = Utf8 I
#25 = Utf8 b
#26 = Utf8 D
#27 = Utf8 c
#28 = Utf8 Ljava/lang/String;
#29 = Utf8 add
#30 = Utf8 (Ljava/lang/String;ID)V
#31 = Utf8 v
#32 = Utf8 SourceFile
#33 = Utf8 Main.java
#34 = NameAndType #12:#13 // "<init>":()V
#35 = Utf8 3
#36 = NameAndType #29:#30 // add:(Ljava/lang/String;ID)V
#37 = Class #46 // java/lang/Integer
#38 = NameAndType #47:#48 // valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
#39 = NameAndType #49:#50 // intValue:()I
#40 = Class #51 // java/lang/System
#41 = NameAndType #52:#53 // out:Ljava/io/PrintStream;
#42 = Class #54 // java/io/PrintStream
#43 = NameAndType #55:#56 // println:(D)V
#44 = Utf8 com/rrtx/adm/Main
#45 = Utf8 java/lang/Object
#46 = Utf8 java/lang/Integer
#47 = Utf8 valueOf
#48 = Utf8 (Ljava/lang/String;)Ljava/lang/Integer;
#49 = Utf8 intValue
#50 = Utf8 ()I
#51 = Utf8 java/lang/System
#52 = Utf8 out
#53 = Utf8 Ljava/io/PrintStream;
#54 = Utf8 java/io/PrintStream
#55 = Utf8 println
#56 = Utf8 (D)V
{
public com.rrtx.adm.Main();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rrtx/adm/Main;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=5, args_size=1
0: iconst_5
1: istore_1
2: ldc2_w #2 // double 2.0d
5: dstore_2
6: ldc #4 // String 3
8: astore 4
10: aload 4
12: iload_1
13: dload_2
14: invokestatic #5 // Method add:(Ljava/lang/String;ID)V
17: return
LineNumberTable:
line 8: 0
line 9: 2
line 10: 6
line 11: 10
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
2 16 1 a I
6 12 2 b D
10 8 4 c Ljava/lang/String;
public static void add(java.lang.String, int, double);
descriptor: (Ljava/lang/String;ID)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=6, args_size=3
0: aload_0
1: invokestatic #6 // Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
4: invokevirtual #7 // Method java/lang/Integer.intValue:()I
7: iload_1
8: iadd
9: i2d
10: dload_2
11: dadd
12: dstore 4
14: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
17: dload 4
19: invokevirtual #9 // Method java/io/PrintStream.println:(D)V
22: return
LineNumberTable:
line 16: 0
line 17: 14
line 18: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 a Ljava/lang/String;
0 23 1 b I
0 23 2 c D
14 9 4 v D
}
结构说明
简单将上边的字节码文件做分类,可以将一个类分成class摘要、常量池、方法栈帧
这几部分
class摘要
摘要主要就是记录class的一些基本信息,包括类的大小修改时间,校验和,编译时候的JDK版本信息,以及类的访问范围等
Classfile /C:/Users/yarne/Desktop/Main.class //来源于解析桌面Main.class文件
Last modified 2021-7-4; size 890 bytes //最后修改时间以及类大小
MD5 checksum 683b4cdcd60108ce5bfdcc9e6fe3c992 //校验和
Compiled from "Main.java"
public class com.rrtx.adm.Main
minor version: 0 //jdk 子版本号
major version: 52 //jdk 主版本号 52代表是java8
flags: ACC_PUBLIC, ACC_SUPER //public类,初始化方法使用父类的
flags标识符对应的含义
标志符名称 | 标志符值 | 释义 |
---|---|---|
ACC_PUBLIC | 0x0001 | Public 类型 |
ACC_FINAL | 0x0010 | Final类型 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 接口修饰符 |
ACC_ABSTRACT | 0x0400 | abstract修饰符 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码生成 |
ACC_ANNOTATION | 0x2000 | 注解修饰符 |
**ACC_ENUM | 0x400 | 枚举修饰符 |
常量池
常量池主要就是包含一个类中所有的基本类型常量以及符号引用,基本类型常量包括字符串常量、final修饰的成员变量、实例变量
等,符号引用包括类和接口的名称、字段的名称和描述、方法的名称和描述
基本类型常量
基本类型常量里面,主要包含的是字面量和引用符,字面量的含义包括文本字符串,final修饰的成员变量,还有数据的值等,对于基本数值int类型的常量,常量池值保存了引用和字面名称,没有保存数据的值
#2 = Double 2.0d //double类型的存了实际值
#4 = String #35 // 引用#35
#35 = Utf8 3 // new String("3")
#23 = Utf8 a //int类型只存了名称,没有存实际值
#24 = Utf8 I
符号引用
符号引用就包含了很多类、接口的名称和描述,字段的名称和描述,方法的名称和描述等等
#5 = Methodref #10.#36 // com/rrtx/adm/Main.add:(Ljava/lang/String;ID)V
#6 = Methodref #37.#38 // java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
#7 = Methodref #37.#39 // java/lang/Integer.intValue:()I
#8 = Fieldref #40.#41 // java/lang/System.out:Ljava/io/PrintStream;
#9 = Methodref #42.#43 // java/io/PrintStream.println:(D)V
#10 = Class #44 // com/rrtx/adm/Main
#11 = Class #45 // java/lang/Object
方法栈帧
方法栈帧就是类里面的方法,每个方法都是一帧,它在类编译之后就被定义好,在线程运行时根据定义进行创建,调用结束后被销毁。
jvm所有的计算操作都是基于栈完成的,每个线程创建出来的同时,都会独享一个自己的线程栈,这个线程栈的作用就是存储栈帧,当前线程中调用的每个方法,jvm都会创建出一个栈帧,每个栈帧中储存着局部变量表、操作栈、动态链接、返回地址以及一些额外的附加摘要信息,栈帧在被调用结束之后销毁
下面是Main.class里所有的栈帧,一共两个方法。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=5, args_size=1
0: iconst_5
1: istore_1
2: ldc2_w #2 // double 2.0d
5: dstore_2
6: ldc #4 // String 3
8: astore 4
10: aload 4
12: iload_1
13: dload_2
14: invokestatic #5 // Method add:(Ljava/lang/String;ID)V
17: return
LineNumberTable:
line 8: 0
line 9: 2
line 10: 6
line 11: 10
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
2 16 1 a I
6 12 2 b D
10 8 4 c Ljava/lang/String;
public static void add(java.lang.String, int, double);
descriptor: (Ljava/lang/String;ID)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=6, args_size=3
0: aload_0
1: invokestatic #6 // Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
4: invokevirtual #7 // Method java/lang/Integer.intValue:()I
7: iload_1
8: iadd
9: i2d
10: dload_2
11: dadd
12: dstore 4
14: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
17: dload 4
19: invokevirtual #9 // Method java/io/PrintStream.println:(D)V
22: return
LineNumberTable:
line 16: 0
line 17: 14
line 18: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 a Ljava/lang/String;
0 23 1 b I
0 23 2 c D
14 9 4 v D
}
栈帧摘要
从栈帧的摘要信息中,基本上可以大概知道一个方法的基本信息
//main方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC //静态的,公共方法
Code:
stack=4, locals=5, args_size=1 //栈帧的深度是4,最大局部变量是5个,入参数量是1个
// add方法
public static void add(java.lang.String, int, double);
descriptor: (Ljava/lang/String;ID)V
flags: ACC_PUBLIC, ACC_STATIC //静态的,公共方法
Code:
stack=4, locals=6, args_size=3 //栈帧的深度是4,最大局部变量是6个,入参数量是3个
局部变量表
局部变量表是一个数组,用来存储当前方法的局部变量,表中可以存储的类型包括boolean、byte、char、short、int、float以及引用类型,因为局部变量是线程私有的,所以不会UC你在线程安全问题,每个栈帧中本地变量表的大小,在.class字节码文件编译完成之后,就已经确定
// main方法 局部变量表
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String; //入参args,类型是String数组
2 16 1 a I // 局部变量a,类型是I int
6 12 2 b D //局部变量b,类型是D double
10 8 4 c Ljava/lang/String; //局部变量c,类型是String
// add方法 局部变量表
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 a Ljava/lang/String; //入参a,类型是String
0 23 1 b I // 入参b,类型是I
0 23 2 c D //入参c,类型是D
14 9 4 v D //计算出的值v ,类型是D
操作数栈
操作数栈也是一个用于计算的栈,方法执行从空栈开始,根据字节码指令不停进行入栈计算,然后出栈,栈的大小也是class编译完成就计算完成的。下面简单看一下main方法操作数栈指令执行流程
//main方法的操作数栈
0: iconst_5
1: istore_1
2: ldc2_w #2 // double 2.0d
5: dstore_2
6: ldc #4 // String 3
8: astore 4
10: aload 4
12: iload_1
13: dload_2
14: invokestatic #5 // Method add:(Ljava/lang/String;ID)V
17: return
指令的分类
根据指令的性质,主要可以分为四个类型:
-
栈操作指令,包括与局部变量交互的指令
-
程序流程控制指令
-
对象操作指令,包括方法调用指令
-
算术运算以及类型转换指令
关键性指令load&store
jvm运行过程中,所有的操作都是在虚拟机栈上进行,但是因为栈的生命周期和线程是一致的,每次计算完毕出栈之后,栈就会被销毁掉,不保留任何数据。所以每次栈操作过程中,会有一个关键性动作,就是从本地变量表中先去加载数据,计算完毕之后,再存储回去
常用指令类型(只说几个上边的)
-
加载以及存储指令load、store
-
常量定义const
-
ldc 复杂类型定义
-
入栈出栈基本类型:iconst、istore、iload中的i,代表意思是int,以次类推
i代表int类型
l代表long
s代表short
b代表byte
c代表char
f代表float
d代表double
a和其他的不一样,a代表的是引用类型
-
invokestatic 调用静态方法
-
出栈 return
操作流程
0: 先定义int类型的常量为5
1: 将这个int类型常量出栈存到slot为1的常量数组里
2: 使用#2引用下的复杂类型定义double类型 2.0
5: 将double类型的常量存到slot为2的常量数组里
6:使用#4引用下的复杂类型定义String类型 字符串3
8: 存到slot为4的常量数组里
10:引用类型载入常量数组中slot为4的值
12:int类型载入常量数组中slot为1的值
13:double类型载入常量数组中slot为2的值
15:调用引用符号为#5的方法帧继续计算
17:出栈
总结
上边简单介绍了字节码的查看方式,字节码文件的结构,以及如何去理解字节码运行结构,上面有提到过,jvm是基于栈计算的,所以了解栈以及运行时结构,可以让我更加深刻的理解jvm栈运行时的动作。
最终用一个比较麻烦的一句话表示线程栈和栈帧之间的调用过程
线程创建->线程栈创建->调用栈帧A->栈帧A创建->栈帧A调用操作数栈->栈帧A操作数栈调用栈帧B->栈帧B创建->栈帧B调用操作数栈->栈帧B返回->栈帧B销毁->栈帧A返回->栈帧A销毁->线程栈销毁->线程销毁
参考:
https://www.lagou.com/lgeduarticle/59364.html
https://segmentfault.com/a/1190000037628881
侵权删除