一、Java程序的"翻译"过程:前端编译与后端编译
Java代码要运行,得经过两次"翻译":
- 前端编译 :把你写的
.java文件翻译成.class文件(字节码),这一步在JVM之外完成,和JVM关系不大,只要最终生成符合规范的.class文件,任何语言都能在JVM上跑。 - 后端编译 :JVM把
.class文件里的字节码指令翻译成操作系统能看懂的机器指令,这才是JVM的核心工作,也是提升性能的关键。
二、字节码怎么执行?解释执行 vs 编译执行
JVM执行字节码有两种方式,各有优缺点:
- 解释执行:像"实时翻译",来一条指令翻译一条,简单但慢(早期Java被吐槽"慢"就是因为这个)。优点是省内存,适合嵌入式、客户端等资源紧张的场景。
- 编译执行:像"提前备课",JVM会把频繁执行的代码(热点代码)提前编译成机器指令,存到缓存(CodeCache)里,下次直接用。快但耗内存,需要"预热"(先解释执行收集信息,再编译)。
HotSpot虚拟机默认用"混合模式":结合两者,既保证启动速度(初期解释执行),又提升运行效率(后期编译热点代码)。
三、怎么找到"热点代码"?热点探测
要编译执行,得先找出哪些代码是"热点"(频繁执行)。JVM用两个计数器判断:
- 方法调用计数器:统计方法被调用的次数,超过阈值(默认10000次)就标记为热点。
- 回边计数器 :统计方法里循环的执行次数(比如
for循环),超过阈值(服务端默认10700次)也会触发编译。
四、谁来编译热点代码?C1和C2编译器
JVM有两个"翻译官",分工合作:
- C1(客户端编译器):初级翻译,编译快、优化简单(比如基础代码优化),适合桌面应用(启动快、占内存少)。
- C2(服务端编译器):高级翻译,编译慢、优化激进(比如复杂代码分析),适合服务器应用(运行效率高,但启动慢、占内存多)。
分层编译:让C1和C2协作,分5个层级逐步优化(比如先C1简单编译,收集信息后C2深度优化),从纯解释执行(层级0)到C2深度优化(层级4),根据代码执行情况动态切换,既保证启动快,又能逐步提升运行效率。平衡启动速度和运行效率。
五、JVM的"性能优化黑科技"
即使你写的代码不够好,JVM也能通过编译优化让它跑得更快,重点有三个:
- 方法内联:减少"函数调用"开销
把小方法的代码直接"复制"到调用它的地方,比如add2(x1,x2)被内联后,add1(x1,x2,x3,x4)就变成x1+x2+x3+x4,不用频繁创建栈帧(函数调用的内存开销)。
- 小技巧 :多写小方法、用
final/private/static修饰方法(方便编译器确定调用目标),能提高内联概率。
- 逃逸分析:让对象"轻装上阵"
判断对象是否会被外部方法/线程引用("逃逸"):
- 不逃逸 :对象只在方法内用,JVM会做优化:
- 标量替换 :把对象拆成基本类型(比如
MyObject(a,b)拆成int a和double b),不用创建完整对象。 - 栈上分配:对象直接在栈上创建(随方法结束销毁),不用进堆(减少GC压力)。
- 标量替换 :把对象拆成基本类型(比如
- 锁消除:去掉"无用的锁"
如果代码里的synchronized锁(比如StringBuffer的append方法)没有多线程竞争,JVM会自动去掉这个锁。比如单线程下用StringBuffer,实际和StringBuilder速度差不多(因为锁被消除了)。
总结
JVM通过"混合执行模式"(解释+编译)、"热点代码识别"、"C1/C2协作编译",再加上"方法内联、逃逸分析、锁消除"等优化技术,让Java程序既能快速启动,又能高效运行,就像一辆兼顾加速和耐力的赛车,不断追求执行效率的极限。