作为 Java 开发者,我们每天都在写 new Object(),但你是否想过,这行简单的代码背后,JVM 到底做了什么?它是如何管理内存的?对象到底被分配到了哪里?
本文将基于 JVM 的运行机制,深入剖析运行时数据区(Runtime Data Area)的内部结构,并还原一个对象从创建到分配内存的完整链路。
一、 什么是 JVM?
编译 + 管理, 简单来说,jvm的作用就是完成对代码的处理,让它运行, 并对一些垃圾进行回收。java之所以可以跨平台也多亏了jvm。

JVM(Java Virtual Machine)是 Java 程序运行的核心环境。它的主要职责可以概括为两点:"翻译官" 和 "大管家"。
-
翻译官:它屏蔽了底层操作系统的差异,让你写的 Java 代码可以"一次编写,到处运行"。
-
大管家:它负责内存的自动分配和垃圾回收(GC),把程序员从繁琐的内存管理中解放出来。
JVM 的宏观运行流程
-
编译 :Java 源代码(
.java)被编译器编译成字节码文件(.class)。 -
加载 :类加载系统 将字节码装载到运行时数据区。
-
运行 :执行引擎 负责将字节码翻译为底层系统指令交由 CPU 执行。
二、 内存结构:运行时数据区

我们常说的 JVM 内存结构,指的就是 运行时数据区 。根据线程私有 和线程共享,可以将其划分为两大类。
1. 线程私有区域(图上蓝色区域)
这部分内存随着线程的创建而创建,随着线程的结束而销毁。这也是垃圾回收(GC)不需要关注的区域。
-
程序计数器 (Program Counter Register)
-
作用 :记住下一条jvm指令的执行地址!从而保证程序执行的有序性!
-
本质:物理上通常通过 CPU 的寄存器实现。
-
特点:它是 JVM 中唯一不会发生内存溢出(OOM)的区域。
-
-
虚拟机栈 (VM Stack)

-
别名 :线程栈。
-
核心组成 :栈帧 (Stack Frame)。每个方法被调用时,都会创建一个栈帧,压入栈中;方法执行完毕,栈帧出栈。
-
栈帧存储内容:方法参数、局部变量表、操作数栈、方法返回地址。
-
异常 :如果方法递归调用过深(如死循环),会导致
java.lang.StackOverflowError。
-
-
本地方法栈 (Native Method Stack)
-
作用 :为
native方法服务。 -
背景:Java 有时无法直接操作底层硬件或系统资源,需要通过 JNI(Java Native Interface)调用 C 或 C++ 编写的本地方法。
-
2. 线程共享区域(图上粉色区域)
这部分是所有线程都能访问的区域,也是 垃圾回收(GC)的主战场。
-
堆 (Heap)

-
作用 :保存对象实例 。它是 JVM 管理的最大一块内存区域。
-
逻辑划分:
-
年轻代 (Young Generation):存放生命周期短的对象。
-
老年代 (Old Generation):存放生命周期长的对象。
-
-
演进:
-
JDK 1.7:堆中还包含永久代(方法区的实现)。
-
JDK 1.8:永久代移除,方法区移至本地内存。
-
-
-
方法区 (Method Area)(可以理解为接口,永久代和元空间可以理解为他的实现类)
-
作用:存储类信息、静态变量、常量、编译后的代码、运行时常量池。
-
版本差异:
-
JDK 1.7 及之前 :实现叫 永久代 (PermGen),占用堆内存,大小固定,容易 OOM。
-
JDK 1.8 及之后 :实现叫 元空间 (Metaspace) ,占用本地内存,大小可自动调整,不再受限于 JVM 堆大小。
-
-
3. 特殊区域:直接内存 (Direct Memory)
-
定义:不属于 JVM 运行时数据区,而是操作系统的本机内存。
-
场景:常见于 NIO 操作。利用 DirectBuffer 避免了数据在 Java 堆和 Native 堆之间来回复制(零拷贝),读写性能高,但分配成本也高。
三、 堆内存的精细化模型:Eden、Survivor 与 Region
为了更好地理解对象分配,我们需要把"堆"这一块拆解得更细致。
1. 经典分代模型
在传统的垃圾收集器(如 Serial, ParNew, CMS)视角下,堆被严格划分为:
-
年轻代 (Young Gen):
-
Eden 区:绝大多数新对象诞生的地方(伊甸园)。
-
Survivor 区 (S0/S1):幸存者区。GC 后没死的对象在这里轮转。
-
-
老年代 (Old Gen):存放长期存活的大对象。
2. G1 收集器的 Region 模型
随着内存越来越大,传统的整块划分导致回收停顿时间过长。G1(Garbage First)收集器引入了 Region 的概念:
-
Region:堆被划分为很多个大小相等的独立区域(Region)。
-
每个 Region 逻辑上依然属于 Eden、Survivor 或 Old,但物理上它们不再是连续的。这让 JVM 可以更灵活地控制回收哪一部分内存。
四、 核心解密:对象的创建与分配流程
当我们执行 new User() 时,对象到底去了哪里?JVM 为了性能,设计了一套严密的**"三级分配策略"**。
举个例子:
java
User user = new User();
这里user在栈上, 而new的 User在堆上, 可以简单理解为user就是一个栈上的指针操控着堆上真实的对象User();
1. 第一级:逃逸分析与栈上分配 (Stack Allocation)
JVM 首先通过逃逸分析技术,判断这个对象的作用域:
-
未逃逸:对象只在当前方法内部使用,不会作为返回值给别人,也不会被其他线程访问。
-
逃逸:对象可能被其他方法或线程引用(如:方法逃逸、线程逃逸)。
策略 : 如果对象未逃逸 ,JVM 可能通过优化,将对象直接分配在栈帧中。
- 优势 :对象随方法结束自动销毁,不需要垃圾回收,极大地减轻了 GC 压力。
2. 第二级:TLAB 分配 (Thread Local Allocation Buffer)
因为多线程下创建对象为了防止冲突需要加锁,所以还不如直接先划分好一个一个的tlab给对象一个专属区域。如果tlab满了就只能eden公共区域去分配,这样的话就需要加锁了。
如果对象必须在堆中分配(发生逃逸或对象过大),JVM 会尝试走"VIP 通道"------TLAB。
-
定义 :在堆的 Eden 区 中,为每个线程预先划分的一小块专属区域。
-
背景:多线程同时在堆中创建对象,为了防止内存冲突,必须加锁,导致性能下降。
-
策略:
-
如果本线程的 TLAB 还有空间,直接在自己的 TLAB 里分配。
-
优势 :无锁分配,效率极高。
-
注意:TLAB 仅在"分配内存"这一刻是线程私有的,分配完成后,对象依然是线程共享的。
-
3. 第三级:公共堆分配 (Common Heap Allocation)
如果 TLAB 满了,或者对象太大 TLAB 装不下:
-
策略 :对象只能在 Eden 区的公共区域 进行分配。
-
代价 :需要加锁,与其他线程竞争内存空间。
总结:对象分配流程图
代码段
graph TD
A[new 对象] --> B{逃逸分析: 是否逃逸?}
B -- 否 --> C[栈上分配]
C --> D[方法结束, 自动销毁]
B -- 是 --> E{TLAB是否够用?}
E -- 是 --> F[TLAB分配 (Eden区专属块)]
E -- 否 --> G[Eden区公共堆分配 (加锁)]
F --> H[堆内存 (GC管理)]
G --> H
结语
JVM 的内存管理不仅仅是简单的"堆和栈"。从 JDK 1.8 元空间的引入,到逃逸分析和 TLAB 的优化,再到 G1 收集器 Region 的设计,所有的演进都是为了在高并发 下实现更快的内存分配 和更高效的垃圾回收。
理解这些,你就不仅仅是在写 Java 代码,而是在掌控代码的运行。