一、编译器
1. 前端编译器(javac)
把 .java → .class 字节码属于 编译期 ,只做语法解析、泛型擦除、语法糖解耦,不做运行期优化。
2. 后端即时编译器(JIT)
运行时把 字节码 → 机器码 属于 JVM 执行子系统 ,带运行期优化(逃逸分析、循环优化、锁消除等)。
二、JVM内置三大编译器
1. 解释器(Interpreter)
逐行解释执行字节码,不编译成机器码。
- 启动快、省内存
- 循环多、热点代码性能差
2. C1 编译器(Client 客户端编译器)
又叫 简单即时编译器
- 编译速度快、优化保守
- 适合桌面客户端、启动快优先
- 做简单优化:常量折叠、方法内联
3. C2 编译器(Server 服务端编译器)
高性能编译器,面试重点
- 编译稍慢,但优化极强
- 服务端默认
- 支持高阶优化:
- 逃逸分析
- 标量替换、栈上分配
- 锁消除、锁粗化
- 循环展开、循环无关外提
三、分层编译(JDK 默认)
解释器 + C1 + C2 配合工作
- 刚开始:解释器先跑,启动快
- 代码跑多了成为热点代码 → 交给 C1 编译
- 继续高频执行 → 升级 C2 深度优化
不用等全量编译,兼顾启动速度 + 运行高性能。
四、热点代码是什么
触发 JIT 编译的两类代码:
- 被多次调用的方法
- 被多次循环执行的循环体
由 计数器 统计热度,阈值到了就触发 JIT 编译优化。
五、JIT 编译器核心优化(必背)
1. 方法内联
(1) 什么是方法内联
把小方法的方法体代码 ,直接复制粘贴 到调用处,消除方法调用本身,不再走:调用 → 栈帧创建 → 参数传递 → 返回 → 栈帧销毁 这套流程。
通俗说:把小方法拆了,代码直接嵌进去用。
(2) 解决问题
- 减少方法调用栈帧开销
- 减少参数压栈、跳转、返回的性能损耗
- 给后续其他优化创造前提(逃逸分析、标量替换、锁消除都依赖内联)
(3) 内联带来的连锁优化
方法内联之后,JVM 才能做:
- 逃逸分析
- 标量替换
- 栈上分配
- 锁消除
- 常量传播、死代码消除
很多高级优化前提就是先做方法内联。
2. 逃逸分析
(1) 作用
分析一个对象的作用域 ,判断对象是否逃逸出当前方法、当前线程:
- 不逃逸:对象只在方法内部使用
- 逃逸:对象被返回、赋值给成员变量、被其他线程引用
(2) 未逃逸对象的3大核心优化
- 栈上分配:对象没有逃逸,直接分配在栈上,方法结束自动销毁,不走 GC,极大减轻堆压力。
- 标量替换:把对象拆解成一个个基本变量,不再创建整体对象,节省内存。
- 同步消除:对象没有多线程共享逃逸,自动把 synchronized 锁消除,提升性能。
(3) 发生对象逃逸的情况
- 对象作为方法返回值返回
- 对象赋值给成员变量、静态变量
- 对象传入多线程、线程池中使用
- 对象被其他类引用持有
3. 锁消除
(1) 定义
某个锁对象没有逃逸 ,只在当前方法、当前线程内部使用,不存在多线程竞争,直接把 synchronized 锁给删掉,消除加锁解锁开销。
(2) 原理
- 逃逸分析:判断锁对象是否只被当前线程访问,不对外逃逸;
- 无共享、无竞争 → 虚拟机判定锁毫无意义;
- JIT 编译时直接抹掉锁逻辑,变成普通代码执行。
4. 锁粗化
(1) 什么是锁粗化
如果连续多次对同一个对象反复加锁、解锁 ,JIT 会把多段小锁合并成一把大锁,减少频繁加解锁的开销。
(2) 触发场景
同一个锁对象,在连续代码块里频繁加解锁:
- 循环里反复加锁
- 连续调用同一个同步方法
(3) 和锁消除区别
- 锁消除:根本没竞争,直接把锁删掉
- 锁粗化 :锁必须保留,只是把多把小锁合并成一把,减少次数
5. 常量折叠
(1) 什么是常量折叠
编译期间直接把常量表达式算出最终结果,运行时不再重复计算。
通俗理解:代码里写死的常量加减乘除,编译器直接帮你算好结果,运行时直接用最终值。
(2) 适用场景
- 基本数据类型常量运算:
int、long、double、boolean - 字符串常量拼接
(3) 关键特点
- 只针对常量,变量不参与折叠
- 发生在 编译期,减轻运行时计算开销
- 属于语法糖 + 编译优化