主题
界面
主题
《计算机组成与二进制》讲了机器看得懂的东西(二进制);本节讲**"人写的东西怎么变成机器能跑的"**:
Java 源码 → 字节码 → (JIT)机器码
↑ ↑ ↑
人看得懂 JVM 看得懂 CPU 看得懂理解这条链路,才能理解后面要学的:JVM 内存结构、类加载、GC、性能调优(《JVM 内存结构与对象创建》 起)✓
javac 与 java 命令吗?(《JDK 与 Java 版本》)✓先看一个真实困惑 ✗:
"Java 是编译型还是解释型语言?"
答"编译型"—— 那它为什么需要一个 JVM 才能运行? ✗
答"解释型"—— 那它为什么要先编译成 .class 文件? ✗两种回答都不完整 ✓ ——Java 是"先编译、后(选择性)编译"的混合模式 ✓✓
★ 为什么这个区分重要:
| 如果要理解… | 就得先懂… |
|---|---|
| 为什么 Java 启动慢 | JIT 需要预热 ★★ |
| 为什么服务跑一会儿会变快 | 热点代码被编译成机器码 ★★ |
为什么有 -Xint / -Xcomp 参数 | 可以强制关掉/打开 JIT ✓ |
| 为什么 AOT 能加快启动 | 省掉编译阶段 ✓ |
| 代 | 代表 | 抽象点 | 人写的是 |
|---|---|---|---|
| 一代:机器语言 | 打孔纸带 | 无抽象 | 0 和 1 ✗ |
| 二代:汇编 | MOV / ADD / JMP | 用助记符代替指令 | 寄存器操作 ✓ |
| 三代:高级语言 | C / Java / Python | 用表达式和结构代替寄存器 | 业务逻辑 ✓✓ |
| 四代:领域语言 / 声明式 | SQL / 正则 / DSL | 描述"要什么"而不是"怎么做" | 意图 ✓✓✓ |
★ 演进的本质是"把复杂度往下推" ✓:
汇编程序员要自己管寄存器
C 程序员要自己管内存
Java 程序员只管对象(GC 管内存)
SQL 程序员只描述结果集(优化器管执行计划)★ 每上一层的代价:
抽象越高,越容易写对,但越难精确控制性能 ✓✓
这就是为什么"追求极致性能"的地方仍然用 C/C++/Rust ✓
三者的执行链路:
① 编译型(C/C++):
源码 --编译器--> 机器码 --直接执行--> CPU
★ 一次编译,运行最快;但换个平台要重新编译 ✗
② 解释型(Python/JS):
源码 --解释器逐行读--> 执行
★ 到哪都能跑;但每行都要"读一遍再执行" ✗
③ 混合型(Java/C#)★★★:
源码 --javac--> 字节码 --JVM 执行-->
├─ 解释执行(前几次)
└─ JIT 编译成机器码(成为热点后)✓✓★ Java 的关键设计:字节码 ✓✓
| 字节码带来的好处 | 说明 |
|---|---|
| 平台无关 ★★★ | 字节码不面向任何 CPU 指令集 ——JVM 负责翻译 ✓ |
| 安全沙箱 ★ | JVM 可以在执行前校验字节码(不允许越界访问内存)✓ |
| 性能优化的空间 ★★ | JVM 运行时才知道"哪段是热点" ——可以针对性优化 ✓✓ |
★ 最后一条是 Java 的隐藏优势:
静态编译(C++)在编译期就要决定一切;JIT 可以在运行时看到"实际的数据分布" ✓
所以长期运行的 Java 服务,峰值性能可以逼近 C++ ✓✓
一段 Java 代码的生命周期:
第 1 次调用 → 解释执行(慢,但启动快)✓
第 100 次 → 被标记为"热点"
第 1000 次 → C1 编译(快速编译,轻度优化)
第 10000 次 → C2 编译(慢速编译,深度优化)★★ 这解释了三个日常现象 ✓✓:
| 现象 | 原因 |
|---|---|
| Java 服务"跑一会儿变快" ★★ | 热点方法被 C2 编译了 ✓ |
| 压测要"预热" ★★★ | 不预热测到的是"解释执行 + 编译过程" ✗(T2-003 就是这么做的)✓ |
| 微基准测试很难写对 ✗ | JIT 会做常量折叠、死代码消除 ——你测的可能不是你想测的 ✗✗ |
★ 用两个 JVM 参数亲眼验证(本节的可运行实验):
# 强制只用解释器:观察耗时显著变长
java -Xint -cp cs-basics/target/classes cn.kb.learn.cs.BinaryLesson
# 默认(混合模式)
java -cp cs-basics/target/classes cn.kb.learn.cs.BinaryLesson★ -Xint 下的差距通常是 3~10 倍 ✓ ——这就是 JIT 的价值 ✓✓
| JIT(即时编译)★ | AOT(提前编译)★ | |
|---|---|---|
| 编译时机 | 运行时 | 构建时 |
| 启动速度 | 慢(要先跑一段)✗ | 快 ✓✓ |
| 峰值性能 | 高(能看运行时数据)✓✓ | 略低(看不到运行时数据)✓ |
| 内存占用 | 大(编译本身要资源)✗ | 小 ✓ |
| 典型 | 传统 Java 服务 ✓ | GraalVM Native Image、云函数 ✓ |
★ 这个取舍解释了"为什么云原生时代 Java 被诟病" ✓:
Serverless / 容器扩缩容场景下,JVM 的"启动几秒 + 预热几十秒"是致命的 ✗
所以 GraalVM Native Image、Spring Boot 3 的 AOT 支持才变得重要 ✓(《云原生》)
★ 但不要盲目崇拜 AOT ✗:
| 场景 | 该选 |
|---|---|
| 常驻的高并发服务 | JIT(峰值性能更重要)✓ |
| 短生命周期函数 / CLI 工具 | AOT(启动速度更重要)✓ |
| 需要动态特性(反射、动态代理) | JIT ✓(AOT 对反射不友好 ✗)★ |
★ 最后一条是实践中的关键:
Spring 大量使用反射与动态代理 ——这曾让 AOT 编译很难做 ✓
所以 Spring 6 / Boot 3 才要做"AOT 友好化改造" ✓
真实项目里选语言,看的不是"哪个最好" ✗ ——而是四个约束 ✓:
| 约束 | 说明 |
|---|---|
| 团队会什么 ★★★ | 招不到人的语言等于没有语言 ✓ |
| 生态与库 ★★ | 有没有成熟的框架、驱动、中间件客户端 ✓ |
| 运行环境 ★ | 部署环境限制(容器大小、内存、启动时间) ✓ |
| 性能要求 | 只有真的"性能敏感"时才考虑它 ⚠️ |
★ 一条经验:
"性能"往往不是选语言的真正理由 ✗ ——80% 的性能问题出在数据库与架构上,不在语言 ✓✓
本项目的实测就印证过这一点:一个内存里的 Servlet,瓶颈在线程调度与 CPU 竞争(T2-003),而不是语言 ✓
# 编译
javac Hello.java
# 反汇编:看到 JVM 指令
javap -c Hello.class你会看到:
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out
3: ldc #13 // String Hello
5: invokevirtual #15 // Method java/io/PrintStream.println★ 这就是字节码 ✓ ——不是机器码,而是一套"面向 JVM 的指令集" ✓✓
# 混合模式(默认)
Measure-Command { java -cp cs-basics/target/classes cn.kb.learn.cs.BinaryLesson }
# 纯解释模式
Measure-Command { java -Xint -cp cs-basics/target/classes cn.kb.learn.cs.BinaryLesson }★ 后者通常慢 3~10 倍 ✓
"Java 编译完就是机器码了" ✗ —— 编译产物是字节码,还要 JVM 翻译 ✓
"Java 一定要 JVM 才能跑?" —— 不一定,GraalVM 可以编成原生可执行文件 ✓本节无新增实验类——它的实验就是用命令行观察 JIT ✓
做法(复现步骤):
cd labs
.\mvnw.cmd -pl cs-basics compile
# ① 看字节码结构
javap -c -cp cs-basics/target/classes cn.kb.learn.cs.BinaryLesson | Select-Object -First 20
# ② 对比 JIT 开与关
$t1 = Measure-Command { java -cp cs-basics/target/classes cn.kb.learn.cs.ComplexityLesson }
$t2 = Measure-Command { java -Xint -cp cs-basics/target/classes cn.kb.learn.cs.ComplexityLesson }
"混合模式: $($t1.TotalSeconds) 秒 | 纯解释: $($t2.TotalSeconds) 秒"★ 预期观察:
| 模式 | 现象 |
|---|---|
| 默认(混合) | 前面的实验慢(解释执行),后面越来越快(JIT 生效) ★★ |
-Xint | 全程都慢,且没有"变快"的过程 ✓ |
★ 这个对比的价值:它让"JIT 是真实存在的机制"从概念变成你亲眼看到的现象 ✓✓
javap -c 看任意类的字节码:观察"一条 Java 语句对应多条字节码" ★;javap -v 看常量池:找到字符串常量与 String 拼接的痕迹 ✓;-XX:+PrintCompilation 参数:观察哪些方法被 JIT 编译、什么时候编译 ★★★;-Xint 跑同一个程序:对比总耗时 ★★;-Xss 调小栈:观察深层递归什么时候栈溢出(《树、堆与哈希表》)✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | 说"Java 是编译型语言" ✗ | 它是混合型(先编译成字节码,再选择性 JIT)✓ |
| 2 | 以为 .class 就是机器码 ✗✗ | 是字节码,需要 JVM 翻译 ✓ |
| 3 | 做微基准时不预热 ✗✗ | 测到的是解释执行与编译过程(《时间复杂度和空间复杂度》)✗ |
| 4 | 以为 AOT 全面优于 JIT ✗ | 常驻服务 JIT 峰值更高 ✓(5.4) |
| 5 | 为"性能"换语言 ✗✗ | 瓶颈通常在数据库与架构 ✓ |
| 6 | 用 -Xint 做性能测试 ✗ | 那是关掉 JIT 的调试模式,不代表真实性能 ✓ |
目标:亲眼看到"字节码"与"JIT"两件事。
步骤:
javap -c 看一个类,找到 invokevirtual 与 ldc ✓;java 与 java -Xint 的总耗时 ★★;-XX:+PrintCompilation,观察编译日志(会看到方法名被逐行打印)★;验收方式:能说出"-Xint 下慢了几倍,以及为什么"。
扩展任务:查一查自己常用的 JDK 发行版:
long start = System.nanoTime();
for (int i = 0; i < 100; i++) { computeOnce(); }
System.out.println((System.nanoTime() - start) / 1_000_000 + " ms");这段基准测试有什么问题? 5. (编程)写一个小程序,让同一个方法被调用 1 次 / 1 万次 / 1000 万次,分别记录单次平均耗时,观察 JIT 生效前后的差异。
javap 与 -XX:+PrintCompilation 观察 ✓。《数据结构:数组、链表、栈与队列》:把"抽象类型"落到真实的耗时上。
暂无