对象的创建
分配内存
虚拟机遇到一条new 指令时,首先将去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析,和初始化过。如果没有,那么必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将新生的对象分配内存,对象所需内存的大小在类加载后便可以确定,为对象分配内存的任务,等同于把一块确定大小的内存从Java堆中划分出来。
指针碰撞:分配内存的一种方式。将分界点指针移动一段与对象大小相同的长度,从而完成内存分配。要求内存空间是规整的,内存直接不能相互交错。
空闲列表:如果内存是相互交错的,那么虚拟机就必须维护一个记录表,表中记录了可用的内存,在分配内存的时候,在表中找到合适的内存,并更新记录表。
采用哪一种分配内存的方式有,堆内存是否规整决定,而堆内存的规整又取决于JavaGC是否有压缩整理内存的功能!
并发异常
如果只有一个指针,通过指针移动来分配内存,那么在并发访问时,可能上一条语句没有执行完,指针移动了一半或是还没有来得及修改,下一条指令就运行了。
1 : 对分配内存空间的动作做同步处理。采用CAS配上失败重试的方式保证更新操作的原子性。
2 : 把内存分配的动作按照线程划分在不同的空间之中执行,即每个线程在Java堆中预先分配一小块内存,Thread Local Allocation Buffer,TLAB(本地线程分配缓冲)。哪个线程需要分配内存,就先在哪个线程的TLAB上分配,只有TLAB用完,需要重新分配新的TLAB时才需要加同步。
对象设置与初始化
分配内存之后,虚拟机将该内存初始化为0(不包括对象头),如果使用TLAB,这一工作也会在TLAB分配时进行。这就是实例对象字段可以不赋初值就可以使用,并访问到0值的原因。
接下来虚拟机对对象进行设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息。(这些信息在对象的Object Header中)
完成设置,new 关键字走完。但是,所有的字段还都为0,接下来进行对对象的初始化操作,调用构造方法,把对象按照程序员自己的意愿初始化。至此对象才算是完全产生出来。
对象的内存布局
Header对象头
对象头有两部分
MarkWord :存储对象自身的运行时数据
哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID
MarkWord被设计成一个非固定的数据结构以便在极小的空间存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
类型指针 : 即对象指向它的类元数据的指针
虚拟机通过这个指针来确定这个对象是哪个类的实例 。另外如果对象是一个数组,那么对象头还有一块要保存数组的长度,因为虚拟机无法从数组的元数据中确定数组的大小。
Instance Data实例数据
存储对象的有效信息,也是代码中所定义的各种类型的内容。无论是父类继承的还是子类定义的,都要保存下来。
Padding对齐填充
对齐填充不是必然存在的,没有特别的含义,只起占位符作用。HotSpot VM 的自动内存管理系统要求对象的起始地址必须是8的整数倍。以此,就需要一个对象所占用的空间是8的整数倍。通过padding填充占位符来实现这一点。
对象的访问定位
Reference数据需要指向对象的地址,而虚拟机规范并没有指定reference的定位方式,目前主流有两种定位方式,句柄和直接指针,
句柄
Java堆中会划分出一块内存来作为句柄池,reference中存储对象的句柄地址,而句柄地址中包含了对象的实例数据与类型数据各自的地址信息。
优点:在对象被移动的时候只会改变句柄的值,而不必改变reference。(垃圾回收时移动对象是非常普遍的行为)
直接地址访问
Java堆大小的布局必须考虑如何放置访问类型数据的相关信息,而reference中存储的就是对象地址。
优点:速度更快。它节省了一次指针定位的时间开销。由于对象的访问十分频繁,因此这也是非常可观的执行成本。