从 CPU 指令到 JVM 进程:彻底讲透 Java 执行 main 方法时,类加载、主线程、栈帧入栈的完整底层逻辑

作者介绍 :本文作者 CodeStats,资深底层技术爱好者,专注计算机体系结构、操作系统内核与 Java 虚拟机实现原理。长期在 CSDN 分享硬核技术文章,致力于用通俗语言讲透 Java 程序从源码到 CPU 执行的完整运行逻辑。

参考文章:本文核心思想可与作者的以下前置文章配合阅读(点击跳转):


📖 目录

  1. 思考一个问题:你真的理解 java -jar 背后发生了什么吗?

  2. 程序执行的终极真相:CPU 只做一件"蠢事

  3. 操作系统与 CPU 权限:进程是"运行中的程序",线程是"执行单元

  4. 思想基础:操作系统如何管理进程与线程?(重点

  5. Java 程序的真相:一个 JVM 进程 + 多个线

  6. 从 main 方法执行看完整内存与栈帧模型(重点

  7. 多线程执行:共享堆 + 私有栈的完美配合

  8. 总结:JVM 是对"计算机 + 操作系统"的完美模拟


一、思考一个问题:你真的理解 java -jar 背后发生了什么吗?

在日常开发中,我们无数次执行:

bash

复制代码
java -jar MyApp.jar

然后 main 方法就跑起来了。但你是否想过:

  • 到底是类加载 先发生,还是 main 方法先入栈?

  • JVM 是一个进程吗?Java 线程和操作系统线程是什么关系?

  • 多个线程之间,堆和栈是如何共享与隔离的?

  • 为什么说 JVM 完美模拟了操作系统?

如果你无法清晰回答这些问题,说明你对 Java 程序运行的完整自洽逻辑 还存在认知空白。

这篇文章将从 CPU 最底层的指令执行 开始,讲到 操作系统进程/线程模型 ,再到 JVM 如何模拟这一模型 ,最后彻底讲透 main 方法执行时的 类加载、主线程创建、栈帧入栈、方法调用链 等完整流程。


二、程序执行的终极真相:CPU 只做一件"蠢事"

2.1 剥离所有软件外衣

我们日常接触的 IDE、Maven、Spring、甚至 JVM 本身,全部是上层建筑。剥离所有软件层,直达硬件,你会发现 CPU 的工作逻辑简单到令人惊讶

CPU 没有理解能力,不会"读懂"任何高级语言。它只会机械地重复一个无限循环:

  1. 查看 程序计数器(PC)------一个记录下一条指令内存地址的寄存器;

  2. 从内存中取出该地址的指令;

  3. 执行指令(算术运算、数据搬移、地址跳转等);

  4. 更新程序计数器,指向下一条指令;

  5. 回到第 1 步。

这就是 冯·诺依曼架构 的核心:

程序就是内存中连续排列的二进制指令;程序运行就是 CPU 顺着程序计数器这条"锁链",逐条取指执行的过程。

2.2 重要推论:CPU 根本听不懂你的 Java 代码

CPU 只能执行它所对应的机器码(x86、ARM、RISC-V 各不相同)。你写的任何高级语言------无论是 Java、Python 还是 JavaScript------最终都必须被翻译成 CPU 能识别的二进制机器语言

这个"翻译"过程,正是 编译型语言解释型语言 的本质分水岭。而 Java 走了一条 中间路线(字节码 + JIT 编译)。


三、操作系统与 CPU 权限:进程是"运行中的程序",线程是"执行单元"

3.1 为什么需要进程和线程?

如果所有程序都能随意访问所有内存和硬件,一个普通程序就能把你的硬盘格式化。为了防止这种灾难:

  • 进程 :操作系统分配资源的基本单位。每个进程有独立的地址空间(你碰不到别人的内存)。

  • 线程 :操作系统调度的基本单位。一个进程内可以有多个线程,共享进程的资源 (堆、文件句柄等),但每个线程有自己的程序计数器

3.2 用户态与内核态:JVM 进程被关在"笼子"里

CPU 设计了特权级别(x86 的 Ring 0 ~ Ring 3):

模式 通俗称呼 能做什么 谁运行在这里
Ring 0 内核态 执行任何 CPU 指令、访问所有内存、直接控制硬件 操作系统内核
Ring 3 用户态 只能访问自己的内存空间,不能直接操作硬件 你的应用程序(包括 JVM 进程)

核心规则 :用户态程序如果想做"特权操作"(读文件、发网络包、申请大块内存),必须通过 系统调用(System Call) 陷入内核,由内核代码代为完成。

这就是为什么你的 Java 程序崩溃不会导致整个电脑蓝屏------它被关在"用户态笼子"里


四、【思想基础】操作系统如何管理进程与线程?(重点)

在讲 Java 之前,必须先理解操作系统本身的进程和线程设计思想。

4.1 进程的本质:虚拟内存 + 页表

操作系统给每个进程一个幻觉:它独占整个内存。

实现方式:

  • 虚拟地址空间 :每个进程从 02^N-1 独立编址

  • 页表:记录虚拟地址 → 物理地址的映射

  • 物理内存:被所有进程分时复用

进程 = 一套独立的页表 + 至少一个执行线程

4.2 线程的本质:共享页表 + 私有栈

同一进程内的多个线程:

  • 共享:页表(地址映射)、堆、全局变量、文件句柄

  • 独享:线程栈、PC 寄存器、一组通用寄存器

线程 = 在同一套地址空间内独立执行的"执行流"

4.3 切换代价的本质:换不换页?(最重要的思想)

这是理解线程"轻量"的关键:

切换类型 是否切换页表 TLB 是否失效 内存页是否切换
进程切换 ✅ 是 ✅ 全部失效 需要页切换
线程切换 ❌ 否 ❌ 保持有效 不需要页切换

为什么进程切换必须"换页"?

  • 不同进程的页表不同,同一虚拟地址映射到不同物理地址

  • 切换进程 = 加载新页表 = 刷新 TLB(地址翻译缓存)

  • TLB 失效后,后续内存访问需要查页表 → 缺页开销

为什么线程切换不换页?

  • 同一进程的线程共用同一个页表

  • 虚拟地址 → 物理地址的映射完全一致

  • 切换线程 = 只保存/恢复 PC、寄存器、栈指针 → 不碰 TLB

核心结论 :进程切换 = 重开销(换页表 + TLB 清空)

线程切换 = 轻量(不换页表,TLB 有效)

这就是为什么:

  • 线程叫"轻量级进程"

  • 多线程比多进程更适合高并发

操作系统思想的总结

OS 用进程隔离资源(独立页表),用线程实现轻量并发(共享页表)。CPU 只负责执行指令,OS 负责决定哪个线程的指令被 CPU 执行。


五、Java 程序的真相:一个 JVM 进程 + 多个线程

5.1 java -jar 到底发生了什么?

执行 java -jar MyApp.jar 时,操作系统视角 看到的不是你的 main 方法,而是:

  1. OS 创建一个新进程 (JVM 进程),加载 java 命令对应的可执行文件(如 /usr/bin/javajava.exe)。

  2. 这个进程的入口点不是 Java 的 main ,而是 JVM 的 C++ 启动代码 (如 java.c 中的 JLI_Launch 函数)。

  3. JVM 启动代码会:

    • 初始化 JVM 内部结构(堆、方法区、线程管理等)

    • 加载 MyApp.jar 中指定的主类

    • 创建主线程

    • 由主线程调用 Java 层的 public static void main(String[]) 方法

关键结论

Java 的 main 方法不是 OS 的进程入口,它是 JVM 进程内主线程调用的一个 Java 方法

5.2 JVM 进程 = 普通操作系统进程

组件 对应 OS 概念
JVM 进程地址空间 进程虚拟内存(堆、方法区、线程栈都在里面)
Java 线程 OS 线程(1:1 映射,由 OS 内核调度)
堆、方法区 进程内共享内存区(所有线程可见)
每个线程的栈 线程私有(存储栈帧)
程序计数器 (PC) 每个线程私有(记录当前执行到哪条字节码指令)

结合第四章的 OS 思想

  • JVM 进程 = 普通 OS 进程 → 拥有独立的页表

  • Java 线程 = OS 原生线程(1:1 映射)→ 共享 JVM 进程的页表

  • Java 线程切换 = OS 线程切换 = 不换页表 = 不需要内存页切换

JVM 并没有发明新的线程模型,它直接使用了操作系统提供的 原生线程库 (如 POSIX 的 pthread 或 Windows 的 CreateThread)。多线程的"轻量",底层依赖的是 OS 线程的页表共享特性。


六、从 main 方法执行看完整内存与栈帧模型(重点)

6.1 类加载:早于 main 的任何指令

顺序不是:先有 main 栈帧。正确的完整顺序是:

  1. JVM 进程启动(OS 分配进程地址空间)

  2. 类加载 :JVM 的类加载器找到并加载 MainClass

    • 加载(查找并读入 .class 文件)

    • 链接(验证、准备、解析)

    • 初始化:执行静态变量赋值、静态代码块

  3. 此时还没有任何栈帧,只有方法区中有了类的元数据。

  4. JVM 创建主线程(主线程的栈初始为空)。

  5. 主线程调用 main → JVM 在主线程栈中压入第一个栈帧main 栈帧)。

一句话总结

类加载发生在 main 方法入栈之前;main 方法是主线程的第一个栈帧。

6.2 主线程创建与 main 栈帧入栈

main 栈帧包含:

  • 局部变量表String[] args,以及方法内定义的局部变量)

  • 操作数栈(用于字节码计算)

  • 方法返回地址main 返回后去哪里,通常回到 JVM 启动代码)

  • 常量池引用(指向当前类的方法区常量池)

6.3 方法调用、入栈、出栈、PC 寄存器回溯

考虑以下代码:

java

复制代码
public class Demo {
    public static void main(String[] args) {
        a();
    }
    static void a() { b(); }
    static void b() { c(); }
    static void c() { System.out.println("end"); }
}

执行过程:

步骤 动作 线程栈状态(从底到顶)
1 主线程调用 main main 栈帧
2 main 调用 a maina
3 a 调用 b mainab
4 b 调用 c mainabc
5 c 执行完毕 c 出栈,PC 回到 b 的调用下一条指令
6 b 执行完毕 b 出栈,PC 回到 a
7 a 执行完毕 a 出栈,PC 回到 main
8 main 执行完毕 main 出栈,栈空,主线程结束

这正是操作系统执行机器码的同一套机制:栈帧 + PC 回溯 + 指令循环。

每个线程都有自己的 PC 寄存器(在 JVM 中指向当前执行的字节码指令地址),当方法调用时,当前 PC 被保存到栈帧中,返回时恢复。

6.4 一个生活比喻

概念 比喻
JVM 进程 一家公司(独立运营,有独立办公区)
主线程 公司的第一位员工
类加载 公司提前买好办公设备、准备好规章制度
main 栈帧 员工开始干第一项任务(填写表格)
方法调用 为了完成大任务,去调用其他小任务(层层细化)
栈帧入栈 在便签本上记录当前做到哪一步,然后去执行子任务
栈帧出栈 子任务完成,撕掉便签,回到上一层继续
多线程 多个员工各自有自己的便签本(私有栈),共享打印机、茶水间(堆)

七、多线程执行:共享堆 + 私有栈的完美配合

text

复制代码
                        ┌─────────────────────────────────┐
                        │         JVM 进程地址空间          │
                        │  ┌─────────────┐ ┌─────────────┐ │
                        │  │   堆(共享)  │ │  方法区(共享)│ │
                        │  │  对象、数组   │ │  类信息、常量 │ │
                        │  └─────────────┘ └─────────────┘ │
                        │                                   │
            ┌───────────┼───────────┬───────────┬───────────┼───────────┐
            │           │           │           │           │           │
            ▼           ▼           ▼           ▼           ▼           ▼
      ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
      │ 线程1栈   │ │ 线程2栈   │ │ 线程3栈   │ │ 线程4栈   │ │ 线程5栈   │
      │ main()   │ │ run()    │ │ run()    │ │ run()    │ │ run()    │
      │ a()      │ │ b()      │ │ b()      │ │ c()      │ │ (空)     │
      │ PC       │ │ PC       │ │ PC       │ │ PC       │ │ PC       │
      └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
           ▲            ▲            ▲            ▲            ▲
           └────────────┴────────────┴────────────┴────────────┘
                            每个线程独立PC、独立栈
  • 每个线程有自己独立的栈:存储该线程的方法调用链和局部变量,互不干扰。

  • 所有线程共享堆:对象在堆上分配,任何线程只要有引用就能访问。

  • 方法区共享:类信息、常量池、静态变量所有线程可见。

  • PC 寄存器私有:每个线程记录自己执行到哪条字节码指令。

调度权在 OS 内核:JVM 的 Java 线程与 OS 线程是 1:1 映射,由 OS 决定哪个线程跑在哪个 CPU 核上。JVM 本身不参与 CPU 时间片分配。

对比第四章的 OS 思想

  • OS 进程内多线程:共享页表 → 不换页 → 轻量切换

  • JVM 进程内多线程:共享堆 + 方法区 → 线程栈私有 → 切换时不碰堆

JVM 的多线程模型,完全遵循 OS 的线程设计思想:共享资源(堆/方法区) + 私有执行上下文(栈/PC)。


八、总结:JVM 是对"计算机 + 操作系统"的完美模拟

回顾整篇文章,我们其实只讲了一件事:

CPU 只会机械执行内存中的机器码。操作系统通过进程和线程管理资源与调度。JVM 则完美模拟了"一台计算机 + 一个操作系统"的模型。

这个统一模型的核心对比:

计算机 / 操作系统 JVM 模拟
CPU 执行机器码 解释器/JIT 执行字节码
PC 寄存器 每个 Java 线程有独立 PC
函数调用栈帧 Java 方法栈帧(含局部变量表、操作数栈)
进程地址空间(含页表) 堆 + 方法区(底层仍是 OS 虚拟内存)
进程切换(换页表、TLB 清空) 不同 JVM 进程之间(由 OS 完成)
线程切换(不换页表) Java 线程切换(1:1 映射到 OS 线程)
系统调用(用户态→内核态) JNI 调用 / native 方法(边界类似)

Java 程序运行的完整自洽闭环

OS 创建 JVM 进程 → JVM 初始化 → 类加载(早于一切方法调用)→ 创建主线程 → 主线程调用 mainmain 栈帧入栈 → 方法调用导致栈帧入栈/出栈 → PC 寄存器回溯 → 多线程各用私有栈、共享堆 → 所有非守护线程结束 → JVM 进程退出

这不是玄学,这是一套经过 30 年验证的"操作系统虚拟化"实现。 JVM 并没有重新发明轮子,而是完美复用了操作系统已有的进程和线程模型,在上面加了一层 平台无关的字节码执行环境


作者寄语

计算机科学没有魔法。所有看似神奇的效果------java -jar 一键启动、多线程自动切换、内存自动回收------底层都是简单的规则层层组合。

  • 编译型语言:源码 → 直接变成机器码 → CPU 直接执行

  • 解释型语言:源码 → 作为数据输入预制程序(解释器) → 解释器的机器码在跑

  • Java:源码 → 字节码 → JVM(一个特殊的预制程序) → JIT 编译后变成机器码

希望本文能帮你搭建起 从硬件 → 操作系统 → JVM → Java 代码 的完整认知桥梁,让你在今后的学习和工作中"胸有成竹"。


👍 如果觉得有收获

  • 点赞 👍 让系统推荐给更多爱钻底层的朋友

  • 收藏 ⭐ 面试 JVM 之前一定用得上

  • 评论 💬 说说你曾经在哪一行代码上对"栈帧"产生了真正的理解

  • 关注 🔔 作者 CodeStats,获取更多硬核底层技术分享

下期预告:《从 JIT 编译到 AOT:Java 执行速度是如何一步步逼近 C++ 的?》

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔2 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
Yana.nice2 小时前
Linux 只保留 30 天内日志(find命令删除日志文件)
linux·运维·chrome
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
DFT计算杂谈6 小时前
无 Root 权限在 Tesla K80 零门槛部署 DeepSeek 大模型
linux·服务器·网络·数据库·机器学习
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf