从 `.java` 到程序真正跑起来:编译、类加载、JIT 与面试高频点(附流程图)
Java程序从源码到运行的核心流程分为编译期和运行期两大阶段。编译期通过javac将.java文件转换为.class字节码,包括词法分析、语法分析、语义分析等步骤。运行期由java命令启动JVM,通过类加载器(ClassLoader)加载主类,经历加载、链接(验证、准备、解析)和初始化(执行static赋值和代码块)三个阶段后调用main方法。字节码执行采用解释器+JIT的混合模式,热点代码会被编
这篇文章按“结构流程”把 Java 程序从源码到运行的每一步串起来:
编译期(javac) → 运行期(java + JVM) → 类加载(ClassLoader) → 执行(解释器 + JIT)
0. 先定一个目标:Java 程序“运行”的定义是什么?
很多人说“运行 Java 文件”,但更精确的说法是:
- JVM 不会直接执行
.java源文件 - JVM 执行的是
.class字节码(或者 jar 包里的 class)
因此,Java 程序跑起来至少经历两段:
- 编译期:
.java→javac→.class(字节码) - 运行期:
java启动 JVM → 类加载/链接/初始化 → 找到main→ 执行字节码(解释 + JIT)
1. 总体流程图:从源码到 JVM 退出(主线必须背)
2. .java 文件的结构:入口在哪里?
一个最小可运行 Java 程序的关键点在于 入口方法:
public class Hello {
public static void main(String[] args) {
System.out.println("hi");
}
}
- 程序入口是
public static void main(String[] args) java命令运行的是“主类的全限定名”,不是文件名- 有包名:
java com.example.Hello - 没包名:
java Hello
- 有包名:
- 一个
.java文件可以写多个类,但public顶层类通常要求和文件名一致(Hello.java对应public class Hello)
3. 编译期:javac 做了什么?为什么是 .class?
3.1 你看到的命令
javac Hello.java
3.2 编译器内部的“结构流程”
典型阶段:
- 词法分析:把字符流切分成 token(关键字、标识符、符号…)
- 语法分析:把 token 组合为 AST(抽象语法树)
- 语义分析:符号解析、类型检查、重载匹配、可达性检查…
- (可选)注解处理器:编译期生成代码或校验(如 Lombok、部分框架)
- 生成字节码:输出
.class
3.3 产物:一个 .java 可能生成多个 .class
- 顶层类一个
.class - 内部类/匿名类常会额外生成多个
.class(如Outer$Inner.class)
为什么 Java 是“跨平台”?
因为输出的是统一的字节码,由不同平台的 JVM 实现去执行/编译成本地机器码。
4. 运行期:java 命令如何启动 JVM?
4.1 命令
java com.example.Hello
这里 com.example.Hello 是 主类的全限定名。
4.2 进程层面发生了什么
- 操作系统启动一个 Java 进程(
java可执行程序) java启动并初始化 JVM- JVM 准备运行环境:类加载器体系、内存结构、线程等
- JVM 根据 classpath/modulepath 定位主类,开始类加载流程
- 类初始化完成后,调用
main
5. 核心重点:类是怎么被“找到并加载”的?(ClassLoader + 三阶段)
这是面试最容易深挖的部分:加载、链接、初始化。
你只要把这三件事说清楚,回答就很稳。
5.1 类加载三大阶段(重点)
5.2 每一步重点解释
Loading(加载)
- 通过 类加载器(ClassLoader) 从 classpath / jar / modulepath 等位置找到
.class - 把字节码读进 JVM,形成 JVM 内部的类结构
Linking(链接)
- 验证:确保字节码合法、安全(避免恶意字节码破坏 JVM)
- 准备:为
static字段分配内存,并赋“默认值” - 解析:把常量池里的“符号引用”解析为可直接定位的“直接引用”
“准备阶段 static 就赋初值了吗?”
准备阶段是默认值,真正的显式赋值发生在初始化阶段(少数编译期常量除外)。
Initialization(初始化)
- 执行:
static字段的显式赋值(如static int x = 10;)static { ... }静态代码块
- 每个类只会初始化一次(通常在首次“主动使用”触发)
6. 什么时候会触发“类初始化”?
关键词:首次主动使用(active use)。
6.1 典型触发点
new一个类:new A()- 调用静态方法:
A.foo() - 访问/设置静态非 final 字段:
A.x - 反射触发:
Class.forName("A") - 初始化子类时,先初始化父类
6.2 不触发或容易误判的情况
- 引用
static final的编译期常量(可能被内联,不触发初始化) - 仅创建数组引用:
A[] arr = new A[10];(通常不触发 A 初始化) - 仅拿到
A.class(通常不算主动使用)
7. 双亲委派与 ClassLoader(为什么能防止伪造核心类?)
7.1 双亲委派是什么?
加载一个类时:
- 先让父加载器尝试加载
- 父加载器找不到,子加载器才自己加载
7.2 为什么要这样做?
- 避免重复加载同名类
- 安全性:保证
java.lang.String这类核心类只由更高层加载器加载,避免应用伪造核心类
7.3 什么时候会“打破委派”?
常见于:
- SPI / 插件化
- 应用隔离(如 Tomcat、多应用容器)
- 自定义类加载器实现特殊来源
“原则与好处”->“为什么现实会打破”(框架/容器需求)。
8. .class 到底怎么执行?解释器 vs JIT(“Java 越跑越快”的原因)
JVM 执行字节码主要靠两套机制协同:
-
解释执行(Interpreter)
- 逐条读取字节码并执行
- 优点:启动快
- 缺点:长期性能一般
-
即时编译(JIT, Just-In-Time)
- JVM 识别热点方法(调用次数多、循环频繁)
- 将热点字节码编译为本地机器码,CPU 直接执行
- 优点:长期性能好
- 常见现象:程序运行一段时间后更快
一句话记忆:
先解释跑起来,再把热点编译加速。
9. 运行时内存结构:对象在哪里?方法调用在哪里?
9.1 一张图建立直觉
9.2 常用的“标准描述”
- 堆(Heap):绝大多数对象实例
- 栈(Java Stack):方法调用形成栈帧,局部变量等
- 元空间(Metaspace):类元信息、常量池等(JDK8+)
- PC 计数器:记录当前线程执行到哪条字节码
- GC:负责回收堆中不再可达对象(注意:资源释放要手动 close)
10. jar 是怎么跑的?(部署场景常见)
你常见的生产运行方式是 jar:
java -jar app.jar
jar 内部通过 MANIFEST.MF 指定入口:
Main-Class: com.example.Main
本质上仍然是:
- 从 jar 找
.class - 类加载 → 初始化 → 调用 main
11. 一个重要的例子:父子类初始化顺序
这段代码解释“初始化触发与顺序”:
class Parent {
static int p = initP();
static { System.out.println("Parent static block"); }
static int initP() {
System.out.println("Parent.p init");
return 1;
}
}
class Child extends Parent {
static int c = initC();
static { System.out.println("Child static block"); }
static int initC() {
System.out.println("Child.c init");
return 2;
}
}
public class Demo {
public static void main(String[] args) {
System.out.println(Child.c);
}
}
预期输出大致顺序为:
- Parent 的静态初始化(字段赋值、static block)
- Child 的静态初始化
- main 打印
重点:
- “初始化子类前先初始化父类”
- “静态字段显式赋值和 static block 都在 Initialization 阶段”
12. 要点清单
Q1:Java 文件是怎么运行的?
答:.java 先经 javac 编译为 .class 字节码;运行时用 java 启动 JVM,类加载器加载主类并经历加载-链接-初始化,然后调用 main,执行引擎解释执行并对热点做 JIT 编译,最终程序结束 JVM 退出。
Q2:类加载过程有哪些阶段?
答:加载(Loading)→ 链接(Linking:验证/准备/解析)→ 初始化(Initialization:static 赋值与 static 代码块)。
Q3:static 字段什么时候赋初值?
答:准备阶段赋默认值,初始化阶段执行显式赋值与 static 块(编译期常量可能被内联是例外)。
Q4:什么时候会触发类初始化?
答:首次主动使用:new、调用静态方法、访问静态非 final 字段、Class.forName、初始化子类先初始化父类。
Q5:解释执行与 JIT 的区别?
答:解释执行启动快但慢;JIT 把热点字节码编译为机器码长期更快,所以 Java 常见“越跑越快”。
Q6:双亲委派有什么作用?
答:避免重复加载并保证核心类安全(防止伪造 java.lang.*),父加载器优先,父找不到才子加载。
Q7:JVM 运行时内存怎么划分?
答:堆放对象;栈放栈帧和局部变量;元空间放类元信息;PC 记录执行位置;GC 回收堆中不可达对象。
13. 面试“回答思路模板”(建议照这个结构说,1 分钟很稳)
你可以按这个顺序回答任何“Java 如何运行”的问题:
- 先总述两段式:编译期
.java → .class,运行期 JVM 执行字节码 - 展开运行期关键:ClassLoader 加载主类 → 链接(验/备/解)→ 初始化(static)
- 入口与执行方式:找到
main,解释执行 + JIT 编译热点 - 补一句内存与收尾:堆/栈/元空间/PC,main 结束且无非守护线程 JVM 退出
这个结构的好处:层次清晰、可扩展、面试官容易顺着你设定的路线追问(而不是乱问)。
14. 一句话速记(临场救命)
javac 编译成 class 字节码,java 启动 JVM,ClassLoader 加载主类并经历加载-链接(验/备/解)-初始化(static),调用 main,解释+JIT 跑起来,对象在堆、调用在栈、类信息在元空间,main 结束 JVM 退出。
参考:你可以继续扩展的方向(进阶/加分)
- classpath 与 modulepath 的区别(模块化系统)
- 反射/动态代理与类加载的关系
- JIT、逃逸分析、内联、锁消除(性能面试)
- GC 算法与调参(高级岗位)
更多推荐



所有评论(0)