从源代码到多线程: Java 内存与并发底层世界探索之旅
亲爱的宿主,欢迎你即将进入 Java 程序运行的底层世界!ヾ(@^▽^@)ノ
小系统已经准备好啦!今天我们不只学习"怎么写代码",还要跟着一行 Java 代码走一遍它的完整人生:从源代码被编译成字节码,到加载进内存,再到多线程并发执行,甚至一路下潜到 CPU、内存条这些硬件的物理层面,看看电容器里的电荷是怎么变成 0 和 1 的。
本篇文章将按照以下路线展开:
一段 Java 源代码是如何被编译并加载进内存的?
JVM 为什么要划分栈、堆、方法区?字符串常量池又是什么?
new 一个对象时,内存里到底发生了什么?垃圾回收器怎么"打扫卫生"?
进程与线程究竟是什么关系?多线程的本质为什么是"多栈"?
CPU 和内存、磁盘是如何协作的?为什么 CPU 从不直接读磁盘?
内存条里的电容器为什么"读一次就要充一次电"?
两个线程同时累加 100 万次,为什么最后打印出来却是 0?
宿主大大,系好安全带,我们正式出发!(๑•̀ㅂ•́)و✧
一、前言:代码运行之前,先认识"舞台"
1.1 代码不是魔法,是数据
宿主大大平时写下的 .java 文件,本质上只是给人看的文本。计算机的 CPU 只认识二进制指令——所以在这份代码真正"跑起来"之前,还需要经历编译、加载、内存划分等一系列幕后工序。
而恰恰是这些幕后工序,决定了后面多线程并发问题的根源。不理解内存模型,就无法真正理解并发。

我们先从编译和内存分配说起。
二、Java 程序执行流程与内存管理
2.1 程序编译与内存分配
第一步:编译。 宿主编写的 Java 源代码文件(.java)需要通过 javac 指令编译为二进制字节码文件(.class),随后由 JVM 加载至内存中执行。
第二步:划分内存。 Java 程序在内存中运行时,JVM 会为其划分出几个相互独立的空间:
|
内存区域 |
存放内容 |
关键词 |
|
方法区(Method Area) |
类信息(类结构、方法字节码)、字符串常量池 |
"类的户口本" |
|
栈(Stack) |
方法执行时的栈帧、局部变量 |
"方法的人生轨迹" |
|
堆(Heap) |
|
"对象的宿舍区" |
其中,类信息存放于方法区——当一个类被加载时,它的结构描述就登记在方法区里,供程序运行期间随时查阅。

2.2 对象创建与垃圾回收机制
对象内存分配:堆里开房,栈里留门牌。 通过 new 关键字创建对象时,实际是在堆区开辟一块内存空间;而栈中的引用变量并不直接保存对象内容,它保存的是一个指针,指向堆中那块内存的地址。
Person p = new Person(); // 堆里开出 Person 对象,栈里的 p 存着它的地址
字符串常量池:能复用绝不新建。 字符串类型的变量比较特殊——字符串存储于方法区的字符串常量池中。创建字符串时,JVM 会先检查池中是否已存在相同内容的字符串:
String a = "hello";
String b = "hello"; // 池中已有 "hello",直接复用,不再开辟新空间
若池中已存在相同字符串则直接复用,避免重复开辟内存空间——这就是为什么 a == b 会返回 true。

自动垃圾回收:无人指向就回收。 Java 程序执行过程中(以及执行完毕后),JVM 的垃圾回收器(GC)会自动回收"无人指向"的堆内存对象——如果一个堆中的对象没有任何栈变量引用它,它就成了"孤儿",GC 会择机回收这块内存。
p = null; // 原来的 Person 对象失去引用,等待 GC 回收
这一点与 C/C++ 需要开发者手动 free/delete 形成鲜明对比:Java 开发者无需手动释放内存,省心,但也别滥用对象——GC 并不是免费的午餐。

三、进程与线程:程序世界的"公司"与"员工"
3.1 进程与线程的定义
进程是资源分配的基本单位。 进程是指正在执行的程序。操作系统每启动一个程序,就会创建一个进程,并为其分配独立的内存空间、时间片及 CPU 资源。可以把它想象成一家公司:公司拥有自己的办公场地(内存)和资源配额。
线程是程序执行的过程。 线程代表程序的执行过程,是 CPU 调度的基本单位。在 Java 中,线程就表现为从 main 方法入栈,到执行完毕出栈的完整生命周期。
一个进程至少包含一个线程(主线程);
多个线程共享同一进程的内存资源(比如同一个堆),但各自拥有独立的执行轨迹。

3.2 多线程实现原理:本质是"多栈"
宿主可能会问:Java 是怎么实现"同时做多件事"的?
小系统揭晓答案:Java 实现多线程的本质,是创建多个独立的栈空间。
每个线程都拥有属于自己的栈,每个栈对应一条独立的执行流——方法在这个栈里入栈、出栈,互不干扰。JVM 启动时只有 main 线程一条执行流,每调用一次 new Thread().start(),就多开辟一个新栈,多一条执行流,从而实现并发执行。
|
概念 |
归属 |
特点 |
|
栈 |
每个线程一个,互不共享 |
存放各自方法的局部变量、执行轨迹 |
|
堆 |
整个进程共享 |
所有线程都能访问其中的对象 |

请宿主记住这张图——堆是共享的,栈是独立的。共享,是并发的甜头,也是并发的祸根。 下一节我们就先去硬件底层看看"为什么并发会出问题"。
四、计算机底层硬件与并发瓶颈
4.1 核心硬件与数据交互
CPU 与内存协同工作。 计算机的核心分工是:
CPU:负责数值运算与逻辑运算,是"计算车间";
内存:负责存储指令和数据,是"临时仓库";
两者通过总线连接进行数据交互。
CPU 从不直接与磁盘打交道。 磁盘上的数据必须先加载到内存,CPU 才能使用。原因很简单——速度差距太悬殊了:
|
存储介质 |
读取量级 |
直观比喻 |
|
CPU 运算 |
纳秒级 |
眨一下眼 |
|
内存读取 |
百纳秒级 |
眨完眼顺手关灯 |
|
磁盘读取 |
毫秒级 |
相当于"睡了一觉醒来" |
毫秒级远慢于 CPU 的纳秒级运算速度,如果 CPU 直接等磁盘,就好比让一位顶级大厨守在菜市场现买菜现做菜——车间会被严重拖慢甚至闲置。所以磁盘数据必须先经内存"中转",CPU 才能全速运转。

4.2 内存读写特性:电容与"破坏性读取"
内存基于电容实现。 内存条(DRAM)由无数个电容器组成,每个电容器通过电荷的有无来表示二进制数据——有电荷是 1,没电荷是 0。这就是内存需要持续供电、断电后数据会丢失(易失性)的根本原因。
破坏性读取机制。 更有趣的是,读取内存数据时,读取动作的电流会把电容里存储的电荷释放掉,导致数据消失。因此每次读取之后,硬件必须重新充电恢复数据,这个特性称为破坏性读取。


理解了硬件的这一层,宿主再回头看 Java 的并发问题时,就会明白:并发问题的根源,就藏在"计算很快、存取有距离、多核各自有缓存"这些物理现实之中。
五、多线程并发案例分析:一个令人迷惑的 0
5.1 并发累加案例演示
来看一个经典小实验。小系统创建两个线程,分别对同一个对象的 count 变量进行 100 万次自增操作:
public class Demo {
static int count = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 100_0000; i++) {
count++;
}
});
t.start();
// 主线程没有等待子线程,直接输出
System.out.println("count = " + count);
}
}
理论上,两个线程各加 100 万次,最终结果应该是 200 万(主线程的那份暂且按下不表)。
然而实际运行,输出却是:
count = 0
5.2 为什么是 0?——线程是相互独立的
答案就藏在第三节的知识点里:线程之间默认不存在等待机制。
执行流程是这样的:
- 主线程执行到
t.start(),创建并启动子线程; - 子线程在自己的独立栈里开始辛辛苦苦累加;
- 主线程不会停下来等它,而是继续执行自己的下一行代码——
System.out.println; - 此时子线程的累加还远远没有开始(或尚未完成),
count还是初始值; - 于是主线程打印出
0,然后潇洒地执行完毕、出栈退场。

5.3 结论:线程独立 ≠ 万事大吉
这个案例给宿主两条结论:
- 线程之间相互独立:主线程启动子线程后会继续执行自身逻辑直至出栈,不会阻塞等待子线程完成任务。如果想让主线程等待,需要显式使用
t.join()这样的机制。
- 共享数据 + 多线程 = 需要同步:即使加了
join()让结果"等出来",count++这种"读—改—写"复合操作在多线程交错执行下依然可能丢失更新。这正是并发编程需要锁、原子类等同步工具的根本原因——也是小系统下一课《Java 并发锁的底层世界》要讲的内容哦。

六、结语:从一行代码看懂并发的前因后果
宿主大大,我们这次旅程从高层一路下潜到底层,复盘一下收获:
- 执行流程:
.java经javac编译为.class字节码,由 JVM 加载进内存执行; - 内存分区:方法区放类信息和字符串常量池,栈放方法执行轨迹,堆放对象——
new在堆开空间,栈变量持指针;常量池能复用则复用;GC 自动回收无人指向的堆对象; - 进程与线程:进程是资源分配单位,线程是执行过程;多线程的本质是多栈——堆共享、栈独立;
- 硬件现实:CPU 与内存靠总线协作,磁盘数据必须先进内存(速度差悬殊);内存靠电容存储 0 和 1,且有"破坏性读取、读后重充"的物理特性;
- 并发初体验:两个线程累加却打印出 0,根源是线程独立执行、互不等待——这为下一课的"锁与同步"埋下了完美的伏笔。
并发问题不是玄学,它是程序结构(共享堆、独立栈)与物理硬件(运算快、存取慢)共同作用下的必然结果。理解了"为什么",下一课我们再来解决"怎么办"。
小系统的 Java 底层第一课就到这里,宿主如果还想深入某个方向——比如 GC 回收算法、线程池、volatile 与缓存一致性——随时召唤小系统继续开课!(≧∇≦)ノ
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)