day2背背面经203:浅谈 Java 虚拟机( ̄︶ ̄)↗

Java 编译反编译之后不会完全还原回源码,
因为 Java 源码编译成字节码后,会进行优化和转换,包括变量分配、指令优化、方法调用等。这就是为什么反编译(javap -v)后的指令看起来不像原始的 Java 源代码。


可以给我一个🆓的大拇哥吗?👍😚

1. 为什么编译反编译不会完全还原源码?

原因如下:

  1. Java 编译器优化

    • 编译器会对代码进行优化,例如常量折叠(constant folding)、方法内联(method inlining)等,使得反编译后不完全和源码一致。
  2. Java 字节码是面向 JVM 的指令

    • Java 编译后变成JVM 可以执行的指令(类似汇编语言),但并不是 Java 语言的直接映射。
  3. 局部变量名被丢弃

    • Java 编译后的 .class 文件不会保留局部变量的名字,除非启用了 -g 选项。
    • 例如:
      int result = a + b;
      
      编译后 .class 文件里只会存储 ILOADIADD 指令,而不会存 result 这个变量名。
  4. 语法糖的去除

    • Java 的一些语法(如 for-each 语法、lambda 表达式、switchString 支持等)在字节码中会转换成普通的 for 循环、匿名类或 if-else 语句,因此反编译后可能会有不同的结构。

2. 解析 Java 字节码

示例:

0:  new           #2   // class Main
3:  dup
4:  invokespecial #3   // Method "<init>":()V
7:  astore_1
8:  aload_1
9:  invokevirtual #4   // Method speak:()V
字节码指令说明
0: new #2创建一个 Main 类的新对象
3: dup复制栈顶的对象引用(因为构造方法执行后,栈里要保留这个对象)
4: invokespecial #3调用 Main 类的构造方法 "<init>":()V
7: astore_1把新创建的 Main 对象存入本地变量 main(即 main = new Main();
8: aload_1加载 main 变量到栈顶(准备调用方法)
9: invokevirtual #4调用 main.speak() 方法

对应的 Java 代码

这段字节码对应的 Java 代码是:

public class Main {
    void speak() {
        System.out.println("Hello");
    }

    public static void main(String[] args) {
        Main main = new Main(); // 对应 `new` 指令
        main.speak(); // 对应 `invokevirtual`
    }
}

3. 详细解释每条字节码指令

(1)创建对象

0: new           #2  // class Main
3: dup
4: invokespecial #3  // Method "<init>":()V

等价于:

Main main = new Main();
拆解分析
  • new #2:分配 Main 类的内存空间(类似 new Main())。
  • dup:复制栈顶的对象引用,确保 invokespecial 调用构造方法后仍有该对象。
  • invokespecial #3:调用 Main 的构造方法 Main()

(2)存储对象

7: astore_1

等价于:

Main main = new Main();
拆解分析
  • astore_1:将 main 变量存入本地变量表(索引 1)。
    • 在 Java 字节码中,astore_x 表示将对象引用存入局部变量:
      • astore_0 -> 存入 第0个 变量槽(通常是 this
      • astore_1 -> 存入 第1个 变量槽(对应 main

(3)调用方法

8: aload_1
9: invokevirtual #4  // Method speak:()V

等价于:

main.speak();
拆解分析
  • aload_1:加载 main 变量到栈顶,准备调用方法。
  • invokevirtual #4
    • 调用 Main.speak() 方法(speak()Main 类中定义)。
    • invokevirtual 用于调用实例方法,并进行动态绑定(多态)。

4. 为什么 invokevirtual 不是 invokespecial

invokevirtual 负责调用普通的实例方法,它会根据运行时类型从**虚方法表(vtable)**中查找方法。因此,即使 speak() 没有被 override,它仍然使用 invokevirtual 来支持未来的多态。

如果 speak()private 或者是构造方法 Main(),它会用 invokespecial 来直接调用。


5. 反编译字节码的方法

如果想更清楚地看到 .class 文件对应的字节码,可以使用以下命令:

javac Main.java   # 先编译成 Main.class
javap -v Main     # 反编译查看字节码

输出示例:

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    0: new           #2                  // class Main
    3: dup
    4: invokespecial #3                  // Method "<init>":()V
    7: astore_1
    8: aload_1
    9: invokevirtual #4                  // Method speak:()V
   12: return

6. 总结

  1. Java 反编译不能完全恢复源码,因为:

    • 编译器优化(变量名丢失、语法糖转换)。
    • Java 字节码是 JVM 指令,而非 Java 代码的直接映射。
  2. 如何解析 newinvokevirtualaload 等指令

    • new 创建对象
    • dup 复制对象引用
    • invokespecial 调用构造方法
    • astore_1 存储对象到局部变量
    • aload_1 加载对象到栈
    • invokevirtual 调用实例方法(支持动态绑定)
  3. 为什么 invokevirtual 而不是 invokespecial

    • invokevirtual 适用于普通实例方法,支持多态。
    • invokespecial 适用于构造方法或 private 方法。

可以给我一个🆓的大拇哥吗?👍😚可以关注我,后续持续更新中……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值