当你写下 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 运行时的整个内存划分其实非常清晰,它主要按照“线程是否共享”被分为了两大类:
🚪 线程私有区域(每个线程独享一份,随线程生灭)
这部分内存是各个线程自己管自己的,基本不需要考虑多线程并发冲突的问题。

  1. Java 虚拟机栈 (JVM Stack): 就是我们刚才说的,对应方法的执行 ()。
  2. 本地方法栈 (Native Method Stack): 逻辑和虚拟机栈一样,只不过它是专门为底层 C/C++ 写的 JNI (Native 方法) 服务的 ()。
  3. 程序计数器 (Program Counter): 相当于当前线程正在执行的“行号指示器” ()。特别注意,这是整个 JVM 内存中唯一一个明确规定不会发生 OOM (Out Of Memory) 的区域 ()。

🏟️ 线程共享区域(所有线程公用,容易产生并发问题)
这部分是大家共用的客厅,也是面试和调优最关注的重点。

  1. Java 堆 (Heap): 最大的区域,刚才已经聊过了,专门存放对象实例 ()。
  2. 方法区 (Method Area): 这是一个非常关键的区域,它主要负责存储类信息、常量、静态变量 ()。在 Java 8 之后,这里发生了一次重大的内存变革——移除了原来的永久代 (PermGen),引入了使用本地内存的元空间 (Metaspace) ()。

ThreadLocal
主要数据都在 Java 堆 (Heap) :

  1. ThreadLocal 对象本身:假设你在代码里写了 ThreadLocal<User> tl = new ThreadLocal<>();。只要是通过 new 关键字创建出来的对象,这个 ThreadLocal 实例本身毫无疑问存放在 Java 堆 (Heap) 中 ()。
  2. 存储的实际数据 (Value): 当你调用 tl.set(new User()) 把数据存进去时,这个 User 对象存在哪? 从底层源码来看,每个 Thread 线程实例内部都有一个名为 ThreadLocalMap 的成员变量。当你调用 set() 时,实际上是以 ThreadLocal 实例为 Key,把 User 对象作为 Value,存到了当前线程对象的这个 Map 里。 既然 Thread 本身也是一个实实在在的 Java 对象,那么这个 ThreadLocalMap 以及它里面装载的 User 数据,自然也全部生存在 Java 堆 (Heap) 中 ()。
  3. 变量的引用 (Reference):如果你是在方法内部声明的 tl 这个局部变量名,那么这个引用(指针)存放于当前线程的 Java 虚拟机栈 (JVM Stack) 的局部变量表中 ()。

理解Key 一样,Value 却不同

  • ThreadLocal 实例 就像是一张“饭卡”。(所以通常被定义为 public static final,大家拿到的饭卡外观都一样,也就是相同的 Key)。
  • Thread 线程 就像是“每一个具体的学生”。
  • ThreadLocalMap 就像是学生衣服上的“私有口袋” ()。

当你调用 threadLocal.get() 的时候,底层发生了什么?

  1. JVM 首先看是谁在调用,获取当前正在运行的线程(抓住那个学生):Thread t = Thread.currentThread();
  2. JVM 伸手去掏这个特定线程自己的口袋:拿到这个线程对象内部的 ThreadLocalMap ()。
  3. 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 用完或者对象太大的时候,才会去共享的堆区加锁分配。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐