javac负责编译,将.java源码静态翻译为JVM可执行的.class字节码;java负责运行,启动JVM动态加载、链接、初始化并执行字节码。

javac 和 java 这两个命令看似只是前后脚执行的工具,实则代表了 Java 程序生命周期中两个本质不同的阶段:**静态翻译**与**动态执行**。它们背后是编译器与虚拟机两大系统分工协作的结果。
javac 是纯前端翻译器,不碰内存也不执行逻辑
javac 只做一件事:把人类可读的 .java 源码,按 Java 语言规范,一步步转成 JVM 能识别的二进制字节码(.class)。它内部走的是标准编译流水线:
- 词法分析:拆出 int、public、+、; 这类基础符号(Token)
- 语法分析:构建抽象语法树(AST),检查 if 没写大括号、return 少了分号等结构问题
- 语义分析:确认变量是否声明过、类型能否赋值、方法参数个数对不对
- 字节码生成:把 AST 映射为指令,比如 System.out.println("hello") 会变成 ldc、getstatic、invokevirtual 一串操作码
整个过程不分配运行时内存,不调用任何方法,也不验证代码“能不能跑通”,只确保“写得合规矩”。哪怕你写了 int x = 1 / 0;,javac 也照常编译——因为除零是运行时才发生的错误。
java 命令启动的是完整虚拟机,真正开始“活起来”
当你敲下 java HelloWorld,不是直接执行 .class 文件,而是启动一个 JVM 进程。这个进程要完成从加载到执行的整套动态流程:
立即学习“Java免费学习笔记(深入)”;
- 类加载:ClassLoader 按需读取 .class 字节流,解析类结构、校验字节码安全性(比如跳转指令不能越界)
- 链接:包括验证、准备(给 static 变量分配内存并设默认值)、解析(把符号引用转为直接引用)
-
初始化:执行
<clinit>方法,真正给 static 变量赋初值、运行 static 块 - 执行:JVM 的解释器逐条读取字节码并模拟执行;热点代码还会被 JIT 编译器编译成本地机器码加速
这时候才会暴露运行期问题:空指针、数组越界、找不到类、堆内存溢出……这些在 javac 阶段完全不可见。
关键差异就体现在“什么时候决定什么”
两者的分水岭在于:javac 决定“代码长得对不对”,java 决定“代码跑得通不通”。几个典型对比:
- final 常量表达式(如
static final int A = 2 + 3;)在 javac 阶段就直接算成 5,字节码里看不到计算过程;普通变量相加则保留在运行时算 - 泛型在 javac 阶段被擦除(
List<String>变成List),运行时 JVM 根本不知道泛型信息 - 重载(overload)由 javac 在编译期根据参数类型静态绑定;重写(override)由 JVM 在运行期根据实际对象类型动态分派
- 注解是否保留到运行期,取决于
@Retention策略:SOURCE 级只在 javac 阶段有效,RUNTIME 级才能被反射读取
看穿它们,最直接的方式就是观察输出产物和报错时机
想验证区别?动手试两步:
- 删掉 main 方法的
static修饰符 → javac 报错:“main method not static” —— 编译期语义检查失败 - 写
String s = null; s.length();→ javac 通过,但运行时抛NullPointerException - 用
javap -c HelloWorld查看字节码 → 你看到的是 javac 的“翻译结果”,不是源码本身 - 加 JVM 参数
-XX:+PrintGCDetails -Xlog:gc*→ 这些日志只有 java 命令启动后才输出,属于运行期行为
本质上,javac 是个“守门人”,守住语言规则;java 是个“舞台总监”,负责让角色真正登场、调度资源、应对突发状况。理解这个分工,就抓住了 Java “编译一次,到处运行”的底层支点。



















