不一定。Java 对象通常分配在堆里,但经过 JVM 优化后,有些对象可能不会真正分配到堆中,甚至可能不会真实创建对象。
你这张图表达的是对象分配的大致判断流程:
new 一个对象 ↓ 是否逃逸? ↓ 未逃逸 → 可能栈上分配 → 栈内存 ↓ 逃逸 / 不能栈上分配 ↓ 是否能使用 TLAB? ↓ 能 → TLAB 分配 → Eden 区中的线程私有小块 ↓ 不能 → 普通堆分配 → Eden / Old 区1. 默认理解:对象是在堆中分配的
我们平时说:
Useruser=newUser();一般会说user这个对象分配在堆中。
更准确地说:
Useruser=newUser();里面有两个东西:
user 变量:在栈帧的局部变量表中 new User() 对象:通常在堆中所以常见理解是:
引用在栈上,对象在堆里但是 JVM 后面做了优化,所以不是绝对的。
2. 逃逸分析:判断对象会不会“跑出去”
逃逸分析就是 JVM 判断一个对象的作用范围。
比如:
publicvoidtest(){Useruser=newUser();user.name="Tom";System.out.println(user.name);}这个user只在test()方法内部使用,方法执行完之后,外面拿不到它。
这种情况叫:
对象没有逃逸JVM 可能会认为:既然这个对象不会被外部访问,那就没必要一定放到堆里。
3. 栈上分配:未逃逸对象可能放到栈里
如果对象没有逃逸,JVM 可能会把它分配到当前方法的栈帧里。
这样有一个好处:
方法执行完,栈帧销毁,对象也跟着销毁不用等 GC 回收,减轻堆内存和 GC 压力。
比如:
publicvoidtest(){Pointp=newPoint(1,2);intsum=p.x+p.y;}如果p没有逃逸,JVM 可能优化成类似:
publicvoidtest(){intx=1;inty=2;intsum=x+y;}甚至连Point对象都不真正创建了。
这叫:
标量替换也就是说,对象被拆成几个普通变量来处理。
4. 什么时候对象会逃逸?
情况一:作为返回值返回
publicUsercreateUser(){Useruser=newUser();returnuser;}这里user被返回给外部方法了,外面还能继续使用它,所以它逃逸了。
方法逃逸情况二:赋值给成员变量
publicclassTest{privateUseruser;publicvoidsetUser(){user=newUser();}}这个对象被保存到了成员变量中,方法结束后对象还可能被使用,所以逃逸了。
情况三:赋值给静态变量
publicclassTest{publicstaticUseruser;publicvoidsetUser(){user=newUser();}}静态变量是类级别共享的,其他线程也可能访问,所以逃逸程度更高。
线程逃逸情况四:传给其他方法
publicvoidtest(){Useruser=newUser();save(user);}这里要看save(user)方法内部怎么用。
如果save()只是读一下,不保存、不返回,可能还不算真正逃逸。
但如果save()把它保存到成员变量、集合、静态变量里,那就逃逸了。
5. 如果不能栈上分配,就进入堆分配流程
当对象逃逸了,或者 JVM 判断不能优化时,对象通常就要分配到堆里。
堆里主要分为:
年轻代 - Eden 区 - Survivor 区 老年代 Old大多数普通新对象会先进入 Eden 区。
6. TLAB 是什么?
TLAB 全称是:
Thread Local Allocation Buffer意思是:
线程本地分配缓冲区它不是一块新的内存区域,而是Eden 区中给每个线程提前划分出来的一小块专属空间。
可以理解为:
Eden 区是一大片公共区域 TLAB 是每个线程在 Eden 中提前圈出来的小地盘比如:
Eden 区 ┌─────────────────────────────┐ │ 线程A的TLAB │ 线程B的TLAB │ 公共区域 │ └─────────────────────────────┘7. 为什么需要 TLAB?
多线程同时创建对象时,如果都去 Eden 区申请空间,就会有并发问题。
比如线程 A 和线程 B 同时执行:
newUser();它们都要在堆中找一块空闲内存。
如果没有控制,可能两个线程都分配到了同一块内存位置,这肯定不行。
所以直接在公共堆中分配对象,需要加锁或者 CAS 控制,会影响性能。
TLAB 的作用就是:
每个线程先在自己的 TLAB 中分配对象 只要 TLAB 没满,就不需要和其他线程竞争所以它提高的是:
对象内存分配的效率8. TLAB 不是“栈内存”
这一点很容易混淆。
TLAB 虽然是线程私有的,但它仍然属于堆。
也就是说:
TLAB 内存 ∈ Eden 区 ∈ 堆所以:
对象分配到 TLAB,本质上还是分配在堆里它只是堆里的一块线程私有分配区域。
9. TLAB 和栈上分配的区别
| 对比 | 栈上分配 | TLAB 分配 |
|---|---|---|
| 位置 | 虚拟机栈的栈帧中 | 堆的 Eden 区中 |
| 是否属于堆 | 不属于堆 | 属于堆 |
| 依赖条件 | 对象未逃逸 | 对象需要堆分配,且 TLAB 有空间 |
| 回收方式 | 方法结束自动销毁 | 由 GC 回收 |
| 目的 | 减少堆分配和 GC | 提高堆内存分配速度 |
一句话区分:
栈上分配:对象可能不进堆 TLAB 分配:对象进堆,但在线程私有的小块堆空间中快速分配10. TLAB 只保证“分配过程”线程安全
你笔记里这句话很关键:
TLAB 的线程安全仅体现在内存分配过程中,与对象本身的线程安全性无关。
什么意思?
比如线程 A 在自己的 TLAB 中创建了对象:
Useruser=newUser();这个过程不需要和其他线程抢 Eden 空间,所以分配很快。
但是如果之后这个对象被多个线程共享:
publicstaticUseruser;publicvoidinit(){user=newUser();}那么其他线程也能访问这个对象。
这时候对象内部字段是否线程安全,和它是不是在 TLAB 中分配没有关系。
也就是说:
TLAB 只解决 new 对象时内存怎么分配的问题 不解决对象被多个线程访问时的数据安全问题11. 最终总结
面试可以这样说:
Java 对象不一定一定分配在堆上。通常情况下,new 出来的对象会分配在堆中,优先在 Eden 区分配;为了提高多线程分配效率,JVM 会为每个线程在 Eden 区划分 TLAB,对象优先在线程自己的 TLAB 中分配。但如果 JVM 通过逃逸分析发现对象没有逃逸出当前方法,可能会进行栈上分配,甚至通过标量替换消除对象分配。因此,对象通常在堆中,但不是绝对一定在堆中。
更短一点:
对象通常分配在堆里; 如果未逃逸,可能栈上分配或被标量替换; 如果进入堆分配,优先尝试在线程自己的 TLAB 中分配; TLAB 属于 Eden 区,所以本质仍然是堆内存。