Java 是编译型还是解释型语言?

#Java#面试八股

Java 是编译型还是解释型语言?

结论先行:Java 两者都是。javac 先把源码静态编译成与平台无关的字节码(bytecode),这一步是编译;运行时字节码先由**解释器(Interpreter)**逐条执行,热点代码再由 **JIT 编译器(Just-In-Time Compiler)**编译成本地机器码,这一步是解释加即时编译。所以准确说法是「编译 + 解释 + 运行时编译」的混合执行模型。

一、四个执行形态的对比

形态代表编译时机启动速度峰值性能
纯编译C / C++ / Rust运行前,一次编译到底快(无编译开销)高
纯解释早期 Python / Shell不编译,逐条翻译慢低
混合(Java)HotSpot先编译字节码,运行时再编译热点中等接近纯编译
提前编译GraalVM Native Image构建期编译为可执行文件极快高但无运行期画像

二、执行链路

Hello.java
   │  javac(编译期:词法/语法/语义分析,生成字节码)
   ▼
Hello.class(平台无关的字节码 + 常量池 + 符号引用)
   │  java 启动,类加载器加载、验证、解析
   ▼
┌──────────── HotSpot 执行引擎 ────────────┐
│  解释器:逐条翻译字节码,立即执行        │
│  C1(Client):快速编译,带少量优化      │
│  C2(Server):激进优化,依赖运行期画像  │
└──────────────────────────────────────────┘
   │  方法调用计数器 / 回边计数器达到阈值
   ▼
热点方法 → 编译为本地机器码 → 后续调用直接执行

三、热点探测与分层编译

HotSpot 用计数器判断热点:方法调用计数器统计方法被调用次数,回边计数器统计循环体回跳次数,两者之和超过阈值即提交编译。JDK 7 之后默认开启分层编译(Tiered Compilation),把编译分成 5 层。

层级执行方式特点
0解释执行收集调用次数与类型画像
1C1 编译,无 profiling简单方法直接定型
2C1 编译,带轻量 profiling过渡层
3C1 编译,带完整 profiling为 C2 采集数据
4C2 编译最优代码,可去优化(deoptimization)回退
// 同一段代码在两个阶段的表现可能完全不同
public class JitWarmup {
    public static int sum(int n) {
        int s = 0;
        for (int i = 0; i < n; i++) {
            s += i;                 // 循环回边被计数,容易被 C2 优化成等差公式
        }
        return s;
    }

    public static void main(String[] args) {
        long t0 = System.nanoTime();
        long r = 0;
        for (int i = 0; i < 200_000; i++) {
            r += sum(1000);          // 前几万次是解释执行,之后才走编译后的代码
        }
        System.out.println(r + " in " + (System.nanoTime() - t0) / 1_000_000 + "ms");
    }
}

四、观察编译行为

# 打印哪些方法被编译、编译耗时与层级
java -XX:+PrintCompilation -cp . JitWarmup | head -20

# 只看 C2 的编译统计
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:-TieredCompilation -cp . JitWarmup

# 强制三种执行模式对比
java -Xint -cp . JitWarmup     # 纯解释,慢一个数量级
java -Xcomp -cp . JitWarmup    # 全部编译,启动极慢
java -Xmixed -cp . JitWarmup   # 默认,混合

PrintCompilation 的输出里,% 表示走了 OSR(栈上替换,循环跑到一半被编译),made not entrant 表示去优化——常见于类型画像失效,比如同一个接口突然出现了新实现。

五、为什么字节码不直接编译成机器码

原因说明
跨平台字节码是 JVM 规范的中间表示,换个平台的 JVM 即可运行,无需重新编译
安全加载期可做字节码验证,拒绝越界访问、类型混淆等非法指令
动态性运行期才知道类路径、实现类、CPU 特性,JIT 能据此做更激进的优化
可观测字节码可被 agent 改写,实现链路追踪、热部署、mock

六、常见追问

追问回答要点
JIT 比静态编译强在哪有运行期画像:能内联虚方法、按实际类型去虚化、按 CPU 指令集生成代码;静态编译只能保守假设
JIT 一定更快吗不一定。启动阶段是解释执行,长时间预热后才反超;短生命周期进程反而是劣势
什么是去优化编译时的乐观假设被推翻(如出现新子类),JVM 丢弃已编译代码回退到解释执行
AOT 和 JIT 的关系AOT 在构建期完成编译,没有运行期画像,靠 PGO 弥补;与 JIT 是互补而非替代
为什么 -Xint 还有人用排查「JIT 优化引入的 bug」、复现只在编译后出现的竞态,或用它反证问题与编译相关

小结

Java 的「编译」指的是 javac 到字节码那一步,「解释」和「即时编译」发生在运行时。理解这套混合模型,才能解释线上为什么会有预热慢、去优化抖动、PrintCompilation 输出异常这些现象。

笔记加载中…