Java对象分配流程与TLAB技术
Java对象分配遵循优先级:先逃逸分析决定是否栈上分配,否则使用TLAB线程本地缓冲区,失败后在Eden区分配,大对象或Eden不足时直接进入老年代。TLAB通过无锁指针碰撞提升并发性能,调优可借助PrintGCDetails、PrintTLAB等参数。
Ja va对象分配优先级为:先逃逸分析决定是否栈上分配;否则走TLAB分配;TLAB失败则在Eden区分配;大对象或Eden不足时直接进入老年代。

很多人以为Ja va里new一个对象就是简单往堆里一扔,其实远没那么简单。对象的最终落脚点,背后是一套精密的优先级决策机制——涉及逃逸分析、对象大小、线程状态,甚至JVM运行时的实时策略。每一步都有明确的尝试路径和兜底方案,我们来拆开看看。
栈上分配:首选但受限的优化路径
JVM在创建对象之前,会先做一步“逃逸分析”——判断这个对象会不会被拿出去共享,或者被其他方法引用。如果它只在当前方法内玩,不跑出去,那么恭喜,它有机会直接走栈上分配。JDK 7+ 默认开启了 -XX:+DoEscapeAnalysis,只要条件满足,对象就会被“打散”——通过标量替换(-XX:+EliminateAllocations)拆成基本类型字段,直接在当前线程的栈帧里安家。方法一结束,栈帧弹出,内存自动释放,GC根本不用管它。这效率,谁用谁知道。
当然,限制也很明显:只适合那些小而短命的局部对象。一旦对象体积大,或者有逃逸行为(比如作为返回值、存入静态集合),这条路就走不通了。
TLAB 分配:多线程下的高效默认路径
没能栈上分配的对象,绝大多数会进入新生代的Eden区。但Eden区是共享的,多线程抢着分配可不行。所以JVM想了个办法:每个线程启动时,从Eden里划出一块专属缓冲区,叫TLAB(Thread Local Allocation Buffer)。默认大小大约是Eden总空间的1%,可以通过 -XX:TLABSize 调整。
分配时,线程只需要移动本地指针(指针碰撞),完全无锁、无竞争,性能极高。TLAB用完了,线程会申请一个新的;如果Eden剩余空间不够,那就退化成在共享Eden区分配,这时候就需要CAS同步了,代价就上来了。这里有个容易误解的点:TLAB只是分配时的优化,它并不改变对象生命周期。GC时,新生代回收仍然会扫描整个Eden区,包括所有线程的TLAB,该回收的还得回收。
Eden 与老年代:兜底与特殊规则
当TLAB这条路走不通,或者干脆不适合用(比如显式关闭了TLAB),分配逻辑就退回到共享堆区域。这时候规则就变得直接了:
- 普通小对象:直接在Eden共享区分配,采用指针碰撞(内存规整时)或空闲列表(内存碎片化时)两种方式。
- 大对象(比如大数组):如果大小超过了
-XX:PretenureSizeThreshold这个阈值,会直接跳到老年代。这么做是为了避免大对象在新生代里反复复制,浪费性能。 - 特殊场景:如果TLAB浪费率过高,或者你显式关掉了TLAB(
-XX:-UseTLAB),那么所有分配都走共享Eden,CAS竞争会变得非常激烈,压测时得特别关注。
如何验证与调优分配行为
分配过程虽然看不见摸不着,但JVM提供了几个好用的参数来帮你“看”到它:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps:这是最基础的,看GC日志里的“allocation failure”触发原因,以及各代占用的变化,能帮你快速定位问题。-XX:+PrintTLAB:专门看TLAB情况。每个线程的TLAB大小、refill次数、浪费率一目了然。如果频繁refill或者空间闲置严重,说明TLAB大小可能需要微调。-Xlog:gc*,gc+ref=debug(JDK 9+):更细粒度的日志,能追踪对象分配位置和晋升路径,适合深度调优。
记住,这些参数不是摆设,真正遇到性能瓶颈时,它们是第一手线索。
