JVM 底层探秘:对象创建的详细流程、内存分配机制解析以及线程安全保障策略
文章目录
- 1. 类加载检查
- 2. 内存分配
- ① 指针碰撞
- ② 空闲列表
- ==线程安全问题==:
- 3. 内存空间初始化
- 4. 对象头设置
- 5. 对象初始化
当Java虚拟机遇到一条
new
指令时,会执行以下步骤来创建对象:

1. 类加载检查
首先检查new
指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,必须先执行相应的类加载过程。
2. 内存分配
内存分配有两种方式:
① 指针碰撞
如果Java堆内存是规整的,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那 个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞
”(Bump The Pointer)。
② 空闲列表
如果Java堆内存不规整,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表
”(Free List)。
内存是否规整取决于垃圾收集器是否带有压缩整理功能。例如,Serial和ParNew等带有Compact过程的收集器采用“指针碰撞”,而CMS这种基于Mark-Sweep算法的收集器通常采用“空闲列表”。
线程安全问题:
除了分配方式外,由于对象创建在虚拟机中是一个非常频繁的行为,此时需要保证在并发环境下的线程安全:如果一个线程给对象 A 分配了内存空间,但指针还没来得及修改,此时就可能出现另外一个线程使用原来的指针来给对象 B 分配内存空间的情况。想要解决这个问题有两个方案:
- 方式一:采用同步锁定,或采用 CAS 配上失败重试的方式来保证更新操作的原子性。
CAS操作的基本逻辑是:
(1)比较某个变量的当前值是否等于预期值。
(2)如果相等,则将该变量更新为新值。
(3)如果不相等,则返回当前值,调用方可以选择重
- 方式二:每个线程在Java堆中预先分配一小块内存,称为
本地线程分配缓冲
(Thread Local AllocationBuffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,TLAB是线程私有的内存缓冲区,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
3. 内存空间初始化
分配内存后,虚拟机会将分配到的内存空间初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。
4. 对象头设置
设置对象头信息,包括对象的类元数据指针、哈希码、GC分代年龄等。
5. 对象初始化
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的<init>()
方法还没有执行,new指令之后会接着执行<init>()
方法,按照程序员的意愿对对象进行初始化。