深入理解 Java 虚拟机:对象的创建过程
类加载
虚拟机遇到一条 new
指令时,首先检查,指令的参数是否能在常量池种定位到一个类的符号引用
,并且检查这个符号引用的类是否已经被加载、解析、初始化过
,如果没有那必须执行相应的类加载过程
。
比如:String str = null;
这就意味着类已经被加载,创建对象时这步类加载就不要执行了
分配内存
在类的加载检查通过后,接下来虚拟机将为新生对象分配内存
。对象所需内存大小
在类加载完成后便可以完全确定
。为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆种划分出来
。
分配方式一:指针碰撞
假设 Java 堆
中的内存
是绝对规整
的,所有用过的内存放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离
,这种分配方式称为“指针碰撞”
。
文字不太好理解,看图:
分配方式二:空闲列表
如果 Java 堆
中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单的进行指针碰撞了,虚拟机就必须维护一个列表
,记录上哪些内存是可用的。在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
,这种分配方式称为“空闲列表”
。
如何选择?
选择哪种分配方式由 Java 堆是否规整
决定,而 Java 堆是否规整又与所采用的垃圾收集器是否带有压缩整理功能
决定。因此,在使用 Serial
、ParNew
等带 Compact
过程的收集器时,系统采用的分配算法是指针碰撞
,而使用 CMS
这种基于 Mark-Sweep 算法(标记 - 清除算法)
的收集器时,通常采用空闲列表
。
GC 标记 - 清除相关算法可参考:JVM 知识点整理:GC垃圾收集器及相关算法(标记 - 清除算法)
线程安全问题
除了空间划分
外,对象创建在虚拟机中也是非常频繁的行为,即使是仅仅改变一个指针所指向的位置,在并发
情况下,也不是线程安全的
,可能出现正在给对象 A 分配内存,指针还没来的及修改,对象 B 又同时使用了原来的指针分配内存的情况。
CAS + 失败重试方法
CAS
操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。
执行 CAS
操作的时候,将内存位置的值与预期原值比较
,如果相匹配
,那么处理器会自动将该位置值更新为新值
。否则,处理器不做任何操作。
处理器不做操作意思就是分配内存失败
了,就重新获取内存,通过执行相同的步骤重试
,直到成功为止
。
线程本地分配缓存区(TLAB)
每个线程
在 Java 堆
中预先分配一小块内存
,称为 线程本地分配缓存区(TLAB)
。哪个线程需要分配内存,就在哪个线程的 TLAB
上分配,只有在 TLAB
用完在分配新的内存时候,才需要同步锁定
,虚拟机中是否采用 TLAB
,通过 -XX:+/-UseTLAB
参数来设定。
PS:就是每个线程在开始时候,优先在内存里割一块内存(划地盘),要分配内存都优先在上面分配,不够了再割块内存出来(同步锁定)
后续工作
内存分配完成后,虚拟机需要将分配的内存空间初始化为零值(初始值)
(不包括对象头)。这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就可以使用,程序能访问这些字段对应的零值。
PS:设置初始值,如:int 类型默认值 0
接下来虚拟机对对象进行必要的设置,例如对象是哪个类的实例
、如何才能找到类的元数据信息
、对象的哈希码
、对象的 GC 分代年龄
等信息,存放在对象头(Object Header)中。
JVM
角度来看,新对象已经产生,但从 Java 程序角度来看,才刚刚开始。后续需要执行 <init>(字节码 class 文件指令)
,把对象按照程序员的意愿进行初始化
,这样一个真正可用的对象才完全产生。