Java 对象创建与内存分配:TLAB 与逃逸分析
一、引言
在 Java 编程中,对象的创建和内存分配是非常基础且关键的操作。理解 Java 对象是如何创建以及内存是如何分配的,对于优化代码性能、减少内存开销至关重要。本文将深入探讨 Java 对象创建过程,并着重介绍 TLAB(Thread-Local Allocation Buffer)和逃逸分析这两个与内存分配优化相关的重要概念。
二、Java 对象创建过程
2.1 类加载检查
当 Java 程序执行到创建对象的代码时,虚拟机首先会检查该对象对应的类是否已经被加载、解析和初始化。如果没有,会先进行类加载的一系列操作,将类的信息加载到方法区。
2.2 分配内存
在类加载检查通过后,接下来就是为对象分配内存。对象所需的内存大小在类加载完成后就已经确定,虚拟机需要在堆上划分出一块足够的内存空间给新对象。内存分配有两种常见的方式:
2.2.1 指针碰撞(Bump the Pointer)
如果 Java 堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间用一个指针作为分界点的指示器,那么分配内存就仅仅是把指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞。
2.2.2 空闲列表(Free List)
如果 Java 堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象,并更新列表上的记录,这种分配方式称为空闲列表。
2.3 初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
2.4 设置对象头
虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象的对象头(Object Header)中。
2.5 执行 init
方法
在上述工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚刚开始,<init>
方法还没有执行,所有的字段都还为零值。所以一般来说,执行 new 指令之后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
三、TLAB(Thread-Local Allocation Buffer)
3.1 概念
TLAB 即线程本地分配缓冲区,是 JVM 为每个线程在 Eden 区预先分配的一块私有内存区域。每个线程在创建对象时,优先在自己的 TLAB 中分配内存,这样可以避免多个线程同时在 Eden 区分配内存时的锁竞争,提高对象分配的效率。
3.2 工作原理
当线程需要分配内存时,首先检查自己的 TLAB 中是否有足够的空间。如果有,就在 TLAB 中直接分配内存,不需要加锁;如果 TLAB 中空间不足,会尝试扩展 TLAB;如果扩展失败,则需要在 Eden 区的公共区域进行分配,此时可能会涉及到锁竞争。
3.3 开启与配置
在 JVM 中,TLAB 默认是开启的。可以通过 -XX:+UseTLAB
显式开启,使用 -XX:TLABSize
来设置 TLAB 的初始大小。
3.4 示例代码及效果
public class TLABExample {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Object();
}
}
}
在上述代码中,每个线程在创建 Object
对象时,会优先使用自己的 TLAB 进行内存分配,从而减少了线程间的竞争,提高了分配效率。
四、逃逸分析(Escape Analysis)
4.1 概念
逃逸分析是一种代码分析技术,它可以分析对象的作用域,判断对象是否会逃出方法或者线程的范围。如果一个对象在方法内部被创建,并且不会被外部方法或线程访问,那么这个对象就没有发生逃逸;反之,则发生了逃逸。
4.2 基于逃逸分析的优化策略
4.2.1 栈上分配
如果一个对象没有发生逃逸,JVM 可以将该对象分配在栈上,而不是堆上。当方法执行结束后,栈上的对象会随着栈帧的弹出而自动销毁,无需进行垃圾回收,从而减少了堆内存的压力和垃圾回收的开销。
4.2.2 同步消除
如果一个对象没有发生逃逸,那么对该对象的同步操作(如 synchronized
块)是多余的,JVM 可以将这些同步操作消除,从而提高程序的执行效率。
4.2.3 标量替换
标量是指不可再分解的基本数据类型,如 int
、long
等。如果一个对象没有发生逃逸,JVM 可以将对象拆分成多个标量,直接在栈上分配这些标量,而不是创建整个对象,进一步减少内存占用。
4.2.4 示例代码及分析
public class EscapeAnalysisExample {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
createObject();
}
}
public static void createObject() {
// 这里创建的对象没有发生逃逸
Object obj = new Object();
// 仅在方法内部使用 obj
}
}
在上述代码中,createObject
方法内部创建的 Object
对象没有发生逃逸。如果 JVM 开启了逃逸分析,可能会将该对象进行栈上分配,避免了堆内存的使用和垃圾回收的开销。
五、总结
Java 对象的创建和内存分配是一个复杂而精细的过程,TLAB 和逃逸分析是 JVM 为了优化对象内存分配而采用的重要技术。TLAB 通过为线程提供私有内存区域,减少了线程间的锁竞争,提高了对象分配的效率;逃逸分析则通过分析对象的作用域,实现了栈上分配、同步消除和标量替换等优化策略,进一步提升了程序的性能和内存使用效率。了解这些知识,有助于编写更高效、更优化的 Java 代码。