jvm字节码详解
2.获取字节码齐清单
java代码:
public class HelloByteCode {
public static void main(String[] args) {
HelloByteCode helloByteCode = new HelloByteCode();
}
}
2.1编译反编译(助记符)
javac HelloByteCode.java
javap -c HelloByteCode
警告: 二进制文件HelloByteCode包含com.zhang.demo.HelloByteCode
Compiled from "HelloByteCode.java"
public class com.zhang.demo.HelloByteCode {
//上边为反编译之后的类, 原类里边只有一个方法 编译反编译后 就出现了两个方法,我们都知道再某些情况下 会自动 生成构造方法,这里验证了构造方法是在编译期间自动生成的 。
public com.zhang.demo.HelloByteCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/zhang/demo/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
}
2.2 查看常量池信息
常量池 大家应该都听说过, 英文是 Constant pool 。这里做一个强调: 大多数时候 指的是 运行时常量池 。但运行时常量池里面的常量是从哪里来的呢? 主要就是由 class 文件中的 常量池结构体 组成的。
major version: 52 代表那个版本?
javap -c -verbose HelloByteCode
警告: 二进制文件HelloByteCode包含com.zhang.demo.HelloByteCode
Classfile /Users/zy1994/Desktop/zhangyang/AAAA-jk/jvm/code/demo/src/main/java/com/zhang/demo/HelloByteCode.class
Last modified 2020-10-30; size 303 bytes
MD5 checksum 2a667aec3dbde61f2a4ffd2412d0acd8
Compiled from "HelloByteCode.java"
public class com.zhang.demo.HelloByteCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // com/zhang/demo/HelloByteCode
#3 = Methodref #2.#13 // com/zhang/demo/HelloByteCode."<init>":()V
#4 = Class #15 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 HelloByteCode.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 com/zhang/demo/HelloByteCode
#15 = Utf8 java/lang/Object
SourceFile: "HeloByteCode.java"
major version: 52 (jdk的版本是从45开始的 所以当前的jdk版本是8)
flags: ACC_PUBLIC, ACC_SUPER
ACC_PUBLIC 说明该类是一个PUBLIC的类 ,ACC_SUPER 仅仅是jdk的历史遗留问题需要一直加上的。
我们可以看到常量池中有大量的#号,这就是通常所说的符号引用,因为在类加载之前jvm是不知道这些类方法的具体地址的,所以就需要用一个符号引用先进性代替,再链接的解析阶段这些符号引用才会转化成具体的引用。
那下边的这天语句来进行解析一下常量池:
- Methodref 当前符号引用(常量引用)指向的是一个方法。
- 记下来可以看到是调用一个类的方法 根据常量表查找可以看到是Object类的init方法 当然后边的注释也进行了解读,方法的返回值 V 就是没有返回值的意思。
2.3 查看方法信息
栈帧(Frame)模型 java的线程栈是由多个栈帧组成的,每一次方法调用都会生成一个栈帧 压入栈中
下图为栈帧的构成:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-73vGGTWq-1604890087278)(/Users/zy1994/Desktop/image-20201030165131966.png)]
栈帧主要有三个部分构成:
- 局部变量数组
- 存储的是局部变量的值或者引用 包含了方法的参数和局部变量的参数 (编译时确定)
- 操作数栈
- FILO的数据结构主要是执行jvm的操作指令
- class引用
2.3.1 局部变量数组&操作数栈
通过一个简单的例子明白操作数栈 局部变量数组的关系:
java 代码
public int add() {
int a = 1;
int b = 2;
int c = a + b;
return c;
}
编译和反编译
javac -g xxx
javap -c -verbose xxx
查看指令之前首先要知道两个常用的指令:
load:从局部变量数组中加载变量
store:将值存入局部变量表中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kuhp2Zn4-1604890087280)(/Users/zy1994/Library/Application Support/typora-user-images/image-20201103164220494.png)]
public int add();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1 //栈的最大深度为2 局部变量数组的 大小为4 方法参数为1
//? 为什么 方法的参数为1?
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: iload_3
9: ireturn
LineNumberTable: //代码与助记符的对应关系
line 10: 0
line 11: 2
line 12: 4
line 13: 8
LocalVariableTable: //局部变量表 存储的是局部变量的的值
Start Length Slot Name Signature
0 10 0 this Lcom/zhang/demo/Test2;
2 8 1 a I
4 6 2 b I
8 2 3 c I
}
如下图所示为这次代码执行的具体过程:(局部变量数组画的是不对的 为什么?)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WzdZIkw2-1604890087281)(/Users/zy1994/Library/Application Support/typora-user-images/image-20201104151457854.png)]
2.3.2创建对象指令
public void get();
Code:
0: new #2 // class com/zhang/demo/MyTest
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
new dup invokespecial三个指令在一起的时候就一定是在创建对象。
new 就是创建一个对象 #2 为该对象的符号引用可以查看到该对象的信息。
dup:指令表示复制栈顶数据并放在当前栈的第一个空闲位置。
invokespecial:当前语境为调用构造方法。
思考:
new 指令就是创建对象的指令,为什么还要dup?
astore_1: a表示对象 store 将当前存储到局部变量表中 1 存储到下标为1的位置。
因为invokespecial指令回默认调用 构造方法 ,也会触发init方法的调用。此时需要弹出栈顶的对象引用,完成构造方法调用,当我们想要执行调用其他的方法时。如果不dup就会出现空指针的问题。
2.3.3 栈内存操作指令
swap、pop、pop2、dup、dup2、dup_x1、dup2_x1
swap:交换栈顶 两个槽位的值。
pop: 移除栈顶一个槽位的值。
pop2:移除栈顶两个槽位的值。
要知道无论是虚拟机栈 还是局部变量数组每一个槽位都是占32个字节的。java中除了double和long占据64个字节,其他的都是32个字节的。
**那么怎么交换两个double或者long类型数据的值?**jvm并没有提供swap2的指令。
dup指令不带x的表示复制栈顶x个槽位的值并且压入栈顶。dup的系数代表要复制的slot数
dup: 复制栈顶元素一个槽位的值并且压入栈顶
dup2: 复制栈顶元素两个槽位的值并且压入栈顶
带x的:(系数理解不变,x和系数联合表示要插入的位置)
dup_x1: 复制栈顶一个槽位的值 ,并且插入栈顶 (1+1)=2个solt下边的位置
dup_x2:复制栈顶一个槽位的值 ,并且插入栈顶 (1+2)=3个solt下边的位置
dup2_x1: 复制栈顶一个槽位的值 ,并且插入栈顶 (2+1)=3个solt下边的位置
dup2_x2: 复制栈顶一个槽位的值 ,并且插入栈顶 (2+2)=4个solt下边的位置
下图为交换一个double(long)类型的值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aOYTSZAN-1604890087283)(/Users/zy1994/Library/Application Support/typora-user-images/image-20201104105642611.png)]
2.3.4 算数运算&流程控制&类型转换指令
Code:
stack=4, locals=6, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpgt 54
8: lconst_0
9: lstore_2
10: lload_2
11: ldc2_w #2 // long 3l
14: lcmp
15: ifgt 48
18: iload_1
19: i2l
20: lload_2
21: ladd
22: lstore 4
24: lload 4
26: ldc2_w #2 // long 3l
29: lcmp
30: ifne 41
33: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
36: lload 4
38: invokevirtual #5 // Method java/io/PrintStream.println:(J)V
41: lload_2
42: lconst_1
43: ladd
44: lstore_2
45: goto 10
48: iinc 1, 1
51: goto 2
54: return
2.3.5 方法调用指令
1.方法的重载和重写
重载(同一个类或者存在继承关系的非私有方法)
- 编译器重载
方法名相同 参数类型不同
-
jvm重载
jvm区分方法 类名 + 方法名 + 方法描述符(参数类型 + 返回类型)
只要以上的存在一个不一样 就不构成重载方法
关于java认为的重载 jvm认为的不是重载的方法 采用桥接的方式进行处理。
桥接:就是为了让Jvm认为是重写重新定义一个与重写方法所有的类型都有一模一样的方法。但是编辑器编译会不通过,所以就在生成class文件的时候生成。
example:
public class Test3 extends Example{
@Override
public Double get(double price, Customer customer) {
return null;
}
}
interface Customer {
boolean isVIP();
}
class Example {
public Number get(double price, Customer customer) {
return null;
}
}
class反编译成助记符号:
public java.lang.Double get(double, com.zhang.demo.Customer);
descriptor: (DLcom/zhang/demo/Customer;)Ljava/lang/Double;
flags: ACC_PUBLIC
Code:
stack=1, locals=4, args_size=3
0: aconst_null
1: areturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this Lcom/zhang/demo/Test3;
0 2 1 price D
0 2 3 customer Lcom/zhang/demo/Customer;
public java.lang.Number get(double, com.zhang.demo.Customer);
descriptor: (DLcom/zhang/demo/Customer;)Ljava/lang/Number;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=4, locals=4, args_size=3
0: aload_0
1: dload_1
2: aload_3
3: invokevirtual #2 // Method get:(DLcom/zhang/demo/Customer;)Ljava/lang/Double;
6: areturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/zhang/demo/Test3;
重写(继承关系类中的非私有方法)
如果方法名和参数类型都一样
static:子类覆盖父类
非static:重写
2.动态绑定和静态绑定
静态绑定:在解析的过程中能够直接识别目标方法 (将符号引用转换成实际引用后就是一个方法的具体地址)
**动态绑定:**需要在运行过程中根据调用者的动态类型来确定目标方法。(方法表的索引)
que:动态绑定需要一个类似于寻址的操作那么相对于静态绑定的方法是不是效率不高?
-
invokestatic
- 调用类的静态方法的指令,方法指令中 效率最高的一个指令
-
invokespecial
- 构造方法
- 父类可见的方法
- 私有方法
- 实现接口的默认方法
-
invokevirtual
- 非私有方法
-
invokeinterface
- 接口方法
-
invokedynamic
- 动态方法
invokestatic、invokespecial 调用的方法都是可以直接识别具体的目标方法,都是静态绑定。
invokevirtual、invokeinterface 调用的方法除了使用final修饰外(该方法不能被继承 是唯一确定的)都是动态绑定。
//极客时间例子
interface 客户 {
boolean isVIP();
}
class 商户 {
public double 折后价格(double 原价, 客户 某客户) {
return 原价 * 0.8d;
}
}
class 奸商 extends 商户 {
@Override
public double 折后价格(double 原价, 客户 某客户) {
if (某客户.isVIP()) { // invokeinterface
return 原价 * 价格歧视(); // invokestatic
} else {
return super.折后价格(原价, 某客户); // invokespecial
}
}
public static double 价格歧视() {
// 咱们的杀熟算法太粗暴了,应该将客户城市作为随机数生成器的种子。
return new Random() // invokespecial
.nextDouble() // invokevirtual
+ 0.8d;
}
}
2.3.6虚方法调用以及优化
在java中因为没有像c++的viutual的关键字,所以java使用invokevirtual和invokeinterface调用的方法都叫做虚方法。(除了final修饰的其他的都是动态绑定的方式。)
方法表
方法表其实就是一个数组(记录了自己所有的方法和继承到的父类的非私有方法)【invokevirtual是父类 invokeinterface是接口】。
举个????????:
public abstract class Passenger {
abstract void passThroughImmigration();
}
class ChinesePassenger extends Passenger {
@Override
void passThroughImmigration() {
}
void test(){
}
}
class ForeignerPassenger extends Passenger {
@Override
void passThroughImmigration() {
}
}
生成的方法表如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FVZq84xt-1604890087284)(/Users/zy1994/Library/Application Support/typora-user-images/image-20201105152606838.png)]
可以得出结论:
因为方法表是有顺序的所以是存在先后关系的,子类的方法表是按照父类方法表的顺序继续下去的。
可以看到抽象类的toString()方法在索引为0的位置,因为它继承了Object类,所以相应的toString()方法就排在当类方法表的第一位。
jvm还有很多的指令例如异常处理指令,并发控制指令,具体的可以参考这篇博客:https://zhuanlan.zhihu.com/p/268626201
3.字节码操作技术asmtools
https://www.cnblogs.com/yelongsan/p/9674723.html
简单????
public static void main(String[] paramArrayOfString)
{
int i = 1;
if (i != 0) {
System.out.println("Hello, Java!");
}
if (i == 1) {
System.out.println("Hello, JVM!");
}
}
执行main方法输出
Hello JVM!
编译成class文件后。
1.使用asmtools.jar 生成jasm文件
java -jar asmtools.jar jdis Foo.class > Foo.jasm
public static Method main:"([Ljava/lang/String;)V"
stack 2 locals 2
{
iconst_1; //手动将此常量改为2
istore_1;
iload_1;
ifeq L14;
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "Hello, Java!";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
L14: stack_frame_type append;
locals_map int;
iload_1;
iconst_1;
if_icmpne L27;
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "Hello, JVM!";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
L27: stack_frame_type same;
return;
}
执行:java -jar asmtools.jar jasm Foo.jasm
可以看到class文件已经被修改
public Foo() {
}
public static void main(String[] var0) {
byte var1 = 2; //修改为2
if (var1 != 0) {
System.out.println("Hello, Java!");
}
if (var1 == 1) {
System.out.println("Hello, JVM!");
}
}
执行class文件
输出:
Hello JAVA!
上一篇: 朴朴超市完成5500万美元B1轮融资