这篇文章按'结构流程'把 Java 程序从源码到运行的每一步串起来: 编译期(javac) → 运行期(java + JVM) → 类加载(ClassLoader) → 执行(解释器 + JIT)
0. 先定一个目标:Java 程序'运行'的定义是什么?
很多人说'运行 Java 文件',但更精确的说法是:
- JVM 不会直接执行
.java源文件
Java 程序运行包含编译期与运行期。编译期通过 javac 生成.class 字节码。运行期由 JVM 启动,经历类加载(加载、链接、初始化)、内存管理及执行引擎(解释器与 JIT)。重点涵盖双亲委派机制、静态初始化顺序、运行时内存结构(堆、栈、元空间)及热点代码优化。梳理了从源码到退出的完整流程,并总结面试高频考点。
这篇文章按'结构流程'把 Java 程序从源码到运行的每一步串起来: 编译期(javac) → 运行期(java + JVM) → 类加载(ClassLoader) → 执行(解释器 + JIT)
很多人说'运行 Java 文件',但更精确的说法是:
.java 源文件.class 字节码(或者 jar 包里的 class)因此,Java 程序跑起来至少经历两段:
.java → javac → .class(字节码)java 启动 JVM → 类加载/链接/初始化 → 找到 main → 执行字节码(解释 + JIT).java 源码
javac 编译器
词法/语法/语义分析(可含注解处理)
输出 .class 字节码
java 命令启动器
创建 JVM 进程
初始化运行环境
定位主类 Main Class
ClassLoader 加载主类
Linking(链接)
验证 / 准备 / 解析
Initialization(初始化)
static 赋值 + static 块
调用 main(String[] args)
执行引擎执行字节码
解释器 + JIT
main 返回且无非守护线程
JVM 退出
.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.Hellojava Hello.java 文件可以写多个类,但 public 顶层类通常要求和文件名一致(Hello.java 对应 public class Hello)javac 做了什么?为什么是 .class?javac Hello.java
典型阶段:
.class.java 可能生成多个 .class.class.class(如 Outer$Inner.class)为什么 Java 是'跨平台'?
因为输出的是统一的字节码,由不同平台的 JVM 实现去执行/编译成本地机器码。
java 命令如何启动 JVM?java com.example.Hello
这里 com.example.Hello 是 主类的全限定名。
java 可执行程序)java 启动并初始化 JVMmain这是面试最容易深挖的部分:加载、链接、初始化。
你只要把这三件事说清楚,回答就很稳。
首次主动使用该类
Loading(加载)
Linking(链接)
Verification(验证)
校验字节码合法/安全
Preparation(准备)
为 static 字段分配内存并赋默认值
Resolution(解析)
符号引用 -> 直接引用
Initialization(初始化)
static 显式赋值
static 块(static {})
类可用:new / 调静态 / 访问静态...
.classstatic 字段分配内存,并赋'默认值''准备阶段 static 就赋初值了吗?'
准备阶段是默认值,真正的显式赋值发生在初始化阶段(少数编译期常量除外)。
static 字段的显式赋值(如 static int x = 10;)static { ... } 静态代码块关键词:首次主动使用(active use)。
new 一个类:new A()A.foo()A.xClass.forName("A")static final 的编译期常量(可能被内联,不触发初始化)A[] arr = new A[10];(通常不触发 A 初始化)A.class(通常不算主动使用)加载一个类时:
java.lang.String 这类核心类只由更高层加载器加载,避免应用伪造核心类常见于:
'原则与好处'->'为什么现实会打破'(框架/容器需求)。
.class 到底怎么执行?解释器 vs JIT('Java 越跑越快'的原因)JVM 执行字节码主要靠两套机制协同:
一句话记忆:
先解释跑起来,再把热点编译加速。
Java 虚拟机 JVM
线程区(每线程独立)
元空间 Metaspace(共享)
类元信息/常量池/方法元数据
执行引擎
解释器 + JIT
Java 栈
栈帧:局部变量/操作数栈/返回地址
堆 Heap(共享)
对象实例(new 出来的)
GC 垃圾回收
PC 程序计数器
本地方法栈
你常见的生产运行方式是 jar:
java -jar app.jar
jar 内部通过 MANIFEST.MF 指定入口:
Main-Class: com.example.Main本质上仍然是:
.class这段代码解释'初始化触发与顺序':
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);
}
}
预期输出大致顺序为:
重点:
答:.java 先经 javac 编译为 .class 字节码;运行时用 java 启动 JVM,类加载器加载主类并经历加载 - 链接 - 初始化,然后调用 main,执行引擎解释执行并对热点做 JIT 编译,最终程序结束 JVM 退出。
答:加载(Loading)→ 链接(Linking:验证/准备/解析)→ 初始化(Initialization:static 赋值与 static 代码块)。
答:准备阶段赋默认值,初始化阶段执行显式赋值与 static 块(编译期常量可能被内联是例外)。
答:首次主动使用:new、调用静态方法、访问静态非 final 字段、Class.forName、初始化子类先初始化父类。
答:解释执行启动快但慢;JIT 把热点字节码编译为机器码长期更快,所以 Java 常见'越跑越快'。
答:避免重复加载并保证核心类安全(防止伪造 java.lang.*),父加载器优先,父找不到才子加载。
答:堆放对象;栈放栈帧和局部变量;元空间放类元信息;PC 记录执行位置;GC 回收堆中不可达对象。
你可以按这个顺序回答任何'Java 如何运行'的问题:
.java → .class,运行期 JVM 执行字节码main,解释执行 + JIT 编译热点这个结构的好处:层次清晰、可扩展、面试官容易顺着你设定的路线追问(而不是乱问)。
javac 编译成 class 字节码,java 启动 JVM,ClassLoader 加载主类并经历加载 - 链接 (验/备/解)-初始化 (static),调用 main,解释+JIT 跑起来,对象在堆、调用在栈、类信息在元空间,main 结束 JVM 退出。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online