1) 从 .java.class:编译成字节码

① 你写的是 Java 源码

比如:

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
System.out.println("hi");
}
}

javac 编译器把源码编译成字节码(Bytecode)

执行:

1
javac Hello.java

会得到:

  • Hello.class

.class 里不是某个 CPU(x86/ARM)的机器指令,而是一种 平台无关 的指令集:JVM 字节码

这就是“一次编译,到处运行”的核心原因:
不同操作系统/CPU 上只要有对应的 JVM 实现,就能执行同一份 .class 字节码。


2) 从启动到执行:JVM 进程、类加载与初始化

运行时:

1
java Hello

发生的事大致是:

① 启动 JVM 进程

java 命令会启动一个进程,创建 JVM(例如 HotSpot JVM),准备运行环境。

② 找到并加载主类(Hello)

JVM 要执行 main,必须先把 Hello.class 装进来。这个过程由 **类加载器(ClassLoader)**完成。

③ 类加载的三个关键阶段

通常分为(概念上):

  1. Loading(加载):把 .class 的二进制数据读入内存,生成 Class 对象

  2. Linking(链接):让类“可用”

    • Verification(验证):检查字节码是否合法、安全(类型、栈、跳转等)

    • Preparation(准备):给 static 变量分配内存并设默认值

    • Resolution(解析):把常量池里符号引用(类名/方法名)变成直接引用(指针/句柄)

  3. Initialization(初始化):执行类的 <clinit>,也就是静态变量的显式赋值 + static {}

④ 类加载器的“父类委派”思想

常见的类加载器体系(不同 JVM 名称略有差异):

  • Bootstrap:加载核心类库(如 java.lang.*

  • Platform/Extension:平台扩展类库

  • Application:应用 classpath 下的类

  • 以及你自定义的 ClassLoader

典型机制:先问父加载器能不能加载,父不行我再加载。这样可以避免核心类被随意替换,提高安全性与一致性。


3) 真正开始跑:解释执行 + JIT 编译(热点编译)

当类加载好,JVM 开始执行 main 方法里的字节码。执行引擎通常包含两条路:

① 解释器(Interpreter)

  • 一条条读取字节码指令,逐条执行

  • 优点:启动快

  • 缺点:长期跑起来速度不如纯机器码

② JIT 编译器(Just-In-Time)

JVM 会在运行中“观察”哪些代码被频繁执行(热点),然后把这些热点字节码 编译为本机机器码,并做大量优化(内联、逃逸分析、去虚拟化、循环优化等)。

于是整体过程常常是:

  • 先解释执行(快启动)

  • 运行一段时间后,热点代码被 JIT 编译(性能越来越高)

这也是为什么很多 Java 程序会有“预热(warm-up)”现象:刚启动时慢一些,跑一会更快。

(很多现代 JVM 还有“分层编译”思路:先用轻量编译快速提升,再用更激进的优化提升峰值性能。)


4) JVM 的内存区域:代码、对象、线程栈分别放哪?

为了让字节码能执行,JVM 运行时会划分一些典型内存区域(概念模型,各 JVM 实现细节会不同):

① 堆(Heap)

  • 存放对象实例new 出来的对象)

  • 通常是 GC 重点管理区域

  • 大多数情况下占用最大

② 虚拟机栈(Java Stack)

  • 每个线程独有

  • 每次方法调用会创建一个 栈帧(Stack Frame)

  • 栈帧里放:局部变量表、操作数栈、返回地址、部分方法调用信息

  • 方法返回后栈帧出栈

StackOverflowError 常见原因:递归过深/栈帧太多。

③ 方法区 / 元空间(Method Area / Metaspace)

  • 存类的元数据:类信息、方法信息、字段信息、运行时常量池等

  • 现代 HotSpot 常用 **Metaspace(元空间)**作为实现之一(具体实现细节会随版本变化)

④ 程序计数器(PC Register)

  • 每个线程一个,用来记录当前执行到哪条字节码指令(线程切换后能继续)

⑤ 本地方法栈(Native Method Stack)

  • 为 JNI / 本地方法服务(调用 C/C++ 等)

5) 垃圾回收(GC):对象怎么被自动释放?

Java 里对象通常不靠你手动 free,而是 JVM 的 **垃圾回收器(GC)**自动回收堆内存。

① 核心思路:可达性分析(Reachability)

从一组 GC Roots 出发(如线程栈中的引用、静态引用等),能一路引用到的对象认为“活着”;不可达的对象就可以回收。

② 分代思想(常见但非绝对)

很多 GC 会把对象按“存活时间/特征”分区域管理:

  • 新创建对象多、死亡快:回收频繁但成本低

  • 存活久的对象:回收频率低但每次更重

③ 停顿与并发

GC 有时会引入 Stop-The-World 停顿;现代 GC 也大量使用并发/并行技术来降低停顿时间。


6) 线程与同步:为什么 Java 多线程能工作?

  • JVM 会把 Java 线程映射到底层 OS 线程(主流实现如此)

  • synchronizedvolatileLock 等同步机制由 JVM + CPU 内存模型 + OS 协作实现

  • Java 内存模型(JMM)规定了可见性、有序性、原子性等规则,保证并发语义一致


7) 一句话总结 Java 代码运行原理

Java 源码先被编译成平台无关的字节码 .class,运行时由 JVM 通过类加载器加载并验证/链接/初始化,然后由解释器执行字节码;热点代码再由 JIT 编译成机器码以获得高性能;对象主要分配在堆上并由 GC 自动回收,方法调用通过线程栈栈帧管理。