1. Java 程序员视角 vs. JVM 内部视角
在 Java 语言层面,创建对象往往就是 new
关键字加上构造方法,看似很简单。但在 JVM 内部,当遇到一条字节码指令 new
时,需要经过以下步骤:
-
检查类是否已经被加载、解析和初始化
-
如果没有,需要先完成类的加载、解析和初始化(这是类加载机制的工作,可以看我写的类加载内篇笔记)。
-
-
为新生对象分配内存
-
虚拟机会从 Java 堆中为对象分配一块连续且足够的空间。
-
分配方式取决于堆是否规整,常见的有「指针碰撞」和「空闲列表」两种:
-
指针碰撞 (Bump the Pointer)
-
若堆中空闲内存是连续的,一边放已使用的内存,一边放空闲的内存,中间是一个分界指针。
-
分配时,只需把指针向空闲方向移动一段对象大小的距离。
-
优点:操作简单,效率高。
-
适用场景:当垃圾收集器具有「压缩整理」功能(如 Serial、ParNew)时,堆会保持规整。
-
-
空闲列表 (Free List)
-
若堆中的空闲内存和已用内存交织在一起,就无法用指针碰撞了。
-
需要维护一个「空闲列表」,找到一块足够大的内存给对象。
-
适用场景:当垃圾收集器采用「清除 (Sweep)」算法(如 CMS)时,堆可能不规整。
-
-
-
-
解决并发下的线程安全问题
-
对象创建在多线程下是非常频繁的操作,如果多个线程同时为对象分配内存会产生冲突。
-
两种方案:
-
CAS + 失败重试:用原子操作保证指针移动的正确性;
-
TLAB (Thread Local Allocation Buffer):为每个线程预先在堆中分配一小块缓冲区(TLAB),这样每个线程只在自己的 TLAB 中分配对象,只有当 TLAB 用完后才需要同步锁定分配新的缓冲区。
-
-
是否开启 TLAB 可通过
-XX:+/-UseTLAB
参数设置。
-
-
将分配到的内存初始化为零值
-
对象的实例字段会在 Java 层面默认使用「零值」(int=0, boolean=false, 对象引用=null 等)。
-
为了保证这一点,在分配时会把分配到的内存(除对象头外)清零。
-
-
设置对象头信息
-
JVM 需要为对象设置「它是哪个类的实例、如何找到类元数据、对象的哈希码、GC 分代年龄、锁标志位」等信息,这些都放在对象头(Object Header)中。
-
若启用偏向锁(Biased Locking),对象头会带有相应标记。
-
-
执行
<init>()
构造方法-
上面步骤完成后,从 JVM 角度看,对象已经生成了,但对于 Java 程序来说,构造方法还没执行,字段值只是默认值。
-
一般情况下(new 指令后跟
invokespecial
来调用构造方法),Java 程序会调用<init>()
,按照程序员的逻辑进行初始化。这样对象才算真正完成构造、可被使用。
-