一行 println 的 4 条 JVM 指令,我扒了字节码才看懂
你以为写的是 Java,实际写的是"栈操作指令集"
图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.forName 和 newInstance 的调用,涉及:
ldc加载类名invokestatic调用 forNameinvokevirtual调用 newInstance
总结
- ❌ Java 源码 ≠ 执行代码
- ✅ JVM 执行的是栈操作指令
- 💡 掌握
javap= 拿到 JVM 的"显微镜"
你在工作中遇到过哪些"代码简单,字节码复杂"的场景?
评论区见 👇
如果对你有帮助,点赞是最大动力,收藏方便复习,关注追更下篇《invokevirtual 的多态秘密》。
参考:
- JVM Spec - Chapter 6
- 《深入理解 Java 虚拟机》第 6 章