第三阶段:JVM 内存结构 (运行时数据区)
因为Java跑在JVM里,C/C++跑在操作系统的直接环境里,它们的数据结构、内存管理完全不同。JNI定义了一套规范,让Java能够安全地调用C/C++函数,也允许C/C++代码调用Java的对象和方法。在Java中,有些底层操作(比如直接操作操作系统内存、调用硬件接口、极高效率的数学运算等)是Java语言本身做不到或效率不够的。如果在极高并发的场景下,1000 个线程同时要在堆里划分内存空间来创
当你写下 User u = new User(); 这行最常见的代码时,等号左边的引用变量 u,和等号右边真正的 User 对象实例,它们分别存放在 JVM 内存的哪个区域呢?u 变量(栈 = 短期存储): 等号左边的引用变量 u,确实存放在栈里。更准确地说,它存放在Java 虚拟机栈 (JVM Stack) 当前运行方法所对应的“栈帧 (Stack Frame)”的局部变量表中 ()。正如你所说“用完就销毁”,当这个方法执行完毕,栈帧立刻出栈,u 也就随之灰飞烟灭了。new User() 对象(堆 = 相对长期): 等号右边真正创建出来的对象实例,生存在Java 堆 (Heap) 中 ()。Java 堆是对象实例分配的绝对主力区域,同时也是垃圾收集器(GC)的主要战场 ()。它确实比栈里的变量活得长,但最终的寿命由 GC 决定(只要还有人引用它,它就能一直活下去;没人要了,就会被 GC 清理掉)。
JVM 内存结构的“五室一厅”
搞懂了最核心的堆和栈,JVM 运行时的整个内存划分其实非常清晰,它主要按照“线程是否共享”被分为了两大类:
🚪 线程私有区域(每个线程独享一份,随线程生灭)
这部分内存是各个线程自己管自己的,基本不需要考虑多线程并发冲突的问题。
- Java 虚拟机栈 (JVM Stack): 就是我们刚才说的,对应方法的执行 ()。
- 本地方法栈 (Native Method Stack): 逻辑和虚拟机栈一样,只不过它是专门为底层 C/C++ 写的 JNI (Native 方法) 服务的 ()。
- 程序计数器 (Program Counter): 相当于当前线程正在执行的“行号指示器” ()。特别注意,这是整个 JVM 内存中唯一一个明确规定不会发生 OOM (Out Of Memory) 的区域 ()。
🏟️ 线程共享区域(所有线程公用,容易产生并发问题)
这部分是大家共用的客厅,也是面试和调优最关注的重点。
- Java 堆 (Heap): 最大的区域,刚才已经聊过了,专门存放对象实例 ()。
- 方法区 (Method Area): 这是一个非常关键的区域,它主要负责存储类信息、常量、静态变量 ()。在 Java 8 之后,这里发生了一次重大的内存变革——移除了原来的永久代 (PermGen),引入了使用本地内存的元空间 (Metaspace) ()。
ThreadLocal
主要数据都在 Java 堆 (Heap) :
ThreadLocal对象本身:假设你在代码里写了ThreadLocal<User> tl = new ThreadLocal<>();。只要是通过new关键字创建出来的对象,这个ThreadLocal实例本身毫无疑问存放在 Java 堆 (Heap) 中 ()。- 存储的实际数据 (Value): 当你调用
tl.set(new User())把数据存进去时,这个User对象存在哪? 从底层源码来看,每个Thread线程实例内部都有一个名为ThreadLocalMap的成员变量。当你调用set()时,实际上是以ThreadLocal实例为 Key,把User对象作为 Value,存到了当前线程对象的这个 Map 里。 既然Thread本身也是一个实实在在的 Java 对象,那么这个ThreadLocalMap以及它里面装载的User数据,自然也全部生存在 Java 堆 (Heap) 中 ()。 - 变量的引用 (Reference):如果你是在方法内部声明的
tl这个局部变量名,那么这个引用(指针)存放于当前线程的 Java 虚拟机栈 (JVM Stack) 的局部变量表中 ()。
理解Key 一样,Value 却不同
ThreadLocal实例 就像是一张“饭卡”。(所以通常被定义为public static final,大家拿到的饭卡外观都一样,也就是相同的 Key)。Thread线程 就像是“每一个具体的学生”。ThreadLocalMap就像是学生衣服上的“私有口袋” ()。
当你调用 threadLocal.get() 的时候,底层发生了什么?
- JVM 首先看是谁在调用,获取当前正在运行的线程(抓住那个学生):
Thread t = Thread.currentThread(); - JVM 伸手去掏这个特定线程自己的口袋:拿到这个线程对象内部的
ThreadLocalMap()。 - JVM 拿着同样的“饭卡”(
ThreadLocal作为 Key),在这个线程私有的 Map 里刷一下,取出里面的钱(Value)()。
结论: 不同的线程,拥有不同的 ThreadLocalMap ()。即便大家用的都是同一张饭卡(同一个 ThreadLocal 实例作为 Key),但因为是在各自不同的 Map 里做查询,所以拿到的 Value 自然是不同的。s
JVM的本地方法栈以及虚拟机方法栈
本地方法栈(Native Method Stack)与Java虚拟机栈(JVM Stack)的作用非常相似。区别在于:虚拟机栈是为JVM执行Java方法服务的,而本地方法栈则是为JVM执行本地(Native)方法服务的。
什么是 Native(本地方法)?
在Java中,有些底层操作(比如直接操作操作系统内存、调用硬件接口、极高效率的数学运算等)是Java语言本身做不到或效率不够的。这时候,Java会借用C或C++写好的库。 你在源码中经常能看到带有 native 关键字的方法,比如 Object.clone()、Thread.start0()、System.arraycopy(),这些方法只有声明没有实现。它们的底层实现就是C/C++代码,这就是Native方法。
什么是 JNI(Java Native Interface)?
JNI(Java本地接口)是Java世界和C/C++世界之间的一座桥梁或者翻译官。 因为Java跑在JVM里,C/C++跑在操作系统的直接环境里,它们的数据结构、内存管理完全不同。JNI定义了一套规范,让Java能够安全地调用C/C++函数,也允许C/C++代码调用Java的对象和方法。
“为native提供JNI服务”到底是在干什么?
当你的Java代码调用了一个 native 方法时,JVM的执行引擎就会通过JNI去寻找对应的C/C++函数。在这个过程中,本地方法栈就开始工作了,它提供的“服务”主要包括:
- 执行环境切换: 程序的执行权限从“Java世界”切换到了“Native世界”。本地方法栈就是Native方法运行时所在的空间。
- 状态保存与参数传递: 当调用发生时,需要保存当前Java程序的状态,并将Java层面的参数(比如一个Java的String对象)通过JNI转换成C/C++能理解的指针或结构体,压入本地方法栈中。
- 生命周期管理: 就像Java方法有栈帧一样,Native方法执行时,它的局部变量、操作数等也保存在本地方法栈中。执行完毕后,再将C/C++的返回结果转换为Java类型,传回给Java虚拟机栈,并销毁本地方法栈中的相关信息。
TLAB:对象分配的“私有绿色通道”
前面我们说“对象都分配在 Java 堆中”,但 Java 堆是所有线程共享的。 如果在极高并发的场景下,1000 个线程同时要在堆里划分内存空间来创建对象,为了避免指针碰撞(大家抢同一块内存),肯定要加同步锁。但这会严重拖慢对象分配的速度。
- JVM 的破局之道(TLAB): JVM 在 Java 堆的新生代(Eden 区)中,为每个线程预先分配了一小块线程私有的内存,叫做 TLAB (Thread Local Allocation Buffer)。
- 当你的代码
new一个对象时,JVM 会优先在这个线程自己的 TLAB 里分配内存,这就完全不需要加锁,速度极快!只有当 TLAB 用完或者对象太大的时候,才会去共享的堆区加锁分配。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)