在Java里,创建对象只需要new 关键字即可。那么JVM是怎么去创建对象的呢?
当JVM 遇上new 关键字?
JVM 遇上new 指令时
第一步 类加载检查
首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个类是否已经被加载。如果没有加载,就去加载类。
简述一下:
在常量池中定位类的符号引用,检查类是否被加载初始化 如果没有被加载就会去加载初始化类
第二步 分配内存空间
类加载检查通过之后,虚拟机将会为新生对象分配内存,对象所需内存大小在类加载完成后就已经确定。为对象分配内存空间的任务等同于把一块确定大小的内存从java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就只是把那个指针向空闲空间那边挪动一个对象大小的距离,这种分配方式称为指针碰撞(Bump the Pointer)。
如果java堆中内存不是规整的,已使用内存和空闲内存相互交错,那就不能指针碰撞了,虚拟机必须维护一个列表,表上记录哪些内存块是可用的,在分配的时候从列表上找到一块足够大的空间划分给对象,并更新表上记录,这种分配方式称为空闲列表(Free List)。选择哪种分配方式由java堆是否规整决定,而java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
简述一下:
类加载检查之后,JVM开始分配内存,分配内存有两种方式:
- 1.内存规整时,采用指针碰撞(Bump the Pointer)
假设 空闲内存在左边,用过的内存在右边,中间就是指针作为分界点。分配内存时,指针就向左边移动一下。 - 2.内存不规整时,采用空闲列表(Free List)
JVM维护一个列表,表上记录内存块使用情况,分配内存时就从表上找一块足够大的内存空间。
内存是否规整取决于垃圾收集器是否有压缩整理功能.
创建对象的线程安全性
为什么创建对象会有线程安全性问题
可能出现正在给对象A分配内存,指针还没来得急修改,对象B又同时使用了原来的指针来分配内存。
解决方案
- 对分配内存空间的动作进行同步处理
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。 - 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲。哪个线程需要分配内存,就在哪个线程上的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB 参数设定。(TLAB是Eden中的一个区域)
第三 初始化内存空间
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。如果使用TLAB,这一过程也可以提前到TLAB分配时进行。
这一操作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用。 程序能访问到这些字段的数据类型所对应的零值。
简述一下:
将内存空间初始化为零值(不包括对象头)。
第四 设置对象头
空间初始化完成之后,开始对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header) 之中。根据虚拟机当前的运行状态不同,如是否启用偏向锁等,对象头会有不同的设置方式。
简述一下:
设置对象头的信息。
第五 执行 init方法
在上面工作都完成之后,从虚拟机的视角来看 ,一个新的对象已经产生了,但从程序的角度来看,对象创建才刚刚开始----< init > 方法还没有执行,所有的字段还为零,所以一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行 < init > 方法初始化。这样一个真正可用的对象才算完全创建完成。
简述一下:
字节码中有ininvokespecial指令
执行< init >方法初始化。
参考资料
摘抄学习于深入理解Java虚拟机
介绍TLAB的一篇博客