一行 `println` 的 4 条 JVM 指令,我扒了字节码才看懂

0 阅读3分钟

一行 println 的 4 条 JVM 指令,我扒了字节码才看懂

你以为写的是 Java,实际写的是"栈操作指令集"

image.png 图1:JVM 运行时数据区全景图(建议右键新标签页查看大图)


System.out.println("hello");
// 实际执行:getstatic → ldc → invokevirtual → return

核心认知:JVM 是栈计算器,不是函数调用机。


1. 先问个问题:JVM 执行的是 .java 文件吗?

不是。

真实链路:.java → javac → .class(字节码)→ JVM 解释/JIT → 机器码

JVM 只认字节码指令(Bytecode),类似 CPU 认汇编。


2. 实战:javap 拆解 class 文件

2.1 准备代码

public class Hello {
    public void say() {
        System.out.println("hello");
    }
}

2.2 编译 & 反编译

javac Hello.java
javap -c Hello        # 看指令
javap -v Hello.class  # 看完整结构(含常量池)

2.3 输出结果

public void say();
    Code:
       0: getstatic     #2    // System.out
       3: ldc           #3    // "hello"
       5: invokevirtual #4    // PrintStream.println
       8: return

一行代码 = 4 条指令,每条都在操作操作数栈


3. 图解:栈的执行过程

结合上面的 JVM 内存结构图,我们聚焦**栈(线程)**区域:

┌─────────────────────────────────────┐
│           栈(线程)                  │
│  ┌─────────────────────────────┐    │
│  │        add() 栈帧            │    │
│  │  ┌─────────────────────┐    │    │
│  │  │     局部变量表       │◄───┼────┼─── a=1, b=2, c=30
│  │  │   (存放方法参数)      │    │    │
│  │  └─────────────────────┘    │    │
│  │  ┌─────────────────────┐    │    │
│  │  │     操作数栈         │◄───┼────┼─── 30(计算结果)
│  │  │   (压栈-计算-出栈)    │    │    │
│  │  └─────────────────────┘    │    │
│  │  ┌─────────────────────┐    │    │
│  │  │     动态链接         │    │    │
│  │  │   (指向常量池引用)    │    │    │
│  │  └─────────────────────┘    │    │
│  │  ┌─────────────────────┐    │    │
│  │  │     方法出口         │    │    │
│  │  │   (返回地址)         │    │    │
│  │  └─────────────────────┘    │    │
│  └─────────────────────────────┘    │
│  ┌─────────────────────────────┐    │
│  │        main() 栈帧           │    │
│  │  ┌─────────────────────┐    │    │
│  │  │     局部变量表       │◄───┼────┼─── demo1, demo2
│  │  └─────────────────────┘    │    │
│  │  ┌─────────────────────┐    │    │
│  │  │     操作数栈         │    │    │
│  │  └─────────────────────┘    │    │
│  └─────────────────────────────┘    │
└─────────────────────────────────────┘

执行流程

初始:操作数栈为空 []

getstatic #2    →  压入 System.out
                栈:[PrintStream]

ldc #3          →  压入 "hello"
                栈:[PrintStream, "hello"]

invokevirtual   →  弹出参数,调用方法
                栈:[]

关键洞察:Java 代码本质是**"压栈-操作-弹出"**的指令流,每个栈帧独立管理自己的操作数栈。


4. class 文件里到底存了啥?

javap -v 看骨骼,对应图中的方法区(元空间)

Constant pool:           // ← 方法区中的常量池
   #2 = Fieldref        System.out
   #3 = String          "hello"
   #4 = Methodref       PrintStream.println

Code:                    // ← 方法字节码
   stack=2, locals=1    // 栈深 2,局部变量 1 个
   0: getstatic #2
   3: ldc #3
   ...

三要素

  • 类元数据:我是谁(Demo 类元信息)
  • 常量池:我依赖谁(静态变量+常量)
  • 方法字节码:我怎么执行

5. 为什么 JVM 要设计成这样?

表格

特性收益
平台无关字节码跑在任何有 JVM 的机器上
延迟链接运行时解析符号引用,支持动态加载
动态绑定invokevirtual运行时寻址 →多态基础

结合内存图看执行引擎

  • 执行引擎读取程序计数器指向的字节码
  • 操作栈帧中的操作数栈
  • 需要对象时访问(new 对象+数组)
  • 调用 native 方法时走本地方法栈

正因为运行时绑定,才能这样玩:

Animal a = new Dog();
a.say();  // 实际调 Dog.say(),字节码里却是 Animal.say

**JVM 怎么做到的?**下篇聊 invokevirtual 的虚方法分派机制。


6. 动手挑战

分析两段代码的字节码差异:

java

复制

// 写法 A:直接 new
Object obj = new String("test");

// 写法 B:反射创建
Object obj = Class.forName("java.lang.String").newInstance();

问题:哪个指令更多?为什么?

点击查看提示

反射多了 Class.forNamenewInstance 的调用,涉及:

  • ldc 加载类名
  • invokestatic 调用 forName
  • invokevirtual 调用 newInstance

总结

  • ❌ Java 源码 ≠ 执行代码
  • ✅ JVM 执行的是栈操作指令
  • 💡 掌握 javap = 拿到 JVM 的"显微镜"

你在工作中遇到过哪些"代码简单,字节码复杂"的场景?

评论区见 👇


如果对你有帮助,点赞是最大动力,收藏方便复习,关注追更下篇《invokevirtual 的多态秘密》。

参考