如何创建、如何布局、如何访问。
Class加载 --> 内存分配 --> 内存初始化 --> 对象初始化.
当VM遇到字节码 new 指令,检查这个指令的参数在常量池能否定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,没有则需要先执行对应的类加载过程。
类加载检查通过后,则为新生对象分配内存,对象所需大小在类加载后便可完全确定。
指针碰撞(Bump The Pointer)
Java堆内存绝对规整,已使用过的内存放在一边,未使用过的内存在一边,中间使用指针作为指示器,则内存分配仅需要移动指针。
空闲链表(Free List)
java堆内存不规整,需要使用链表维护哪些内存是空闲的,哪些已使用过,分配时从空闲链表找到足够大内存划分对象实例,并更新链表。
选择哪种方式 ---> Java堆是否规整 ---> 所采用的的垃圾收集器是否带有空间压缩整理(Compact)。
Serial、ParNew 等带有空间压缩整理,使用指针碰撞,简单高效。
CMS 采用清除算法(Sweep)实现,使用空闲链表。
实际上即便不规整也会两种均使用,空闲链表管理大块内存,然后在大块内存中采用指针碰撞分配。
CMS 实现中使用了 Linear Allocation Buffer 的分配缓冲器区,先通过空闲链表获取大块分配缓冲区,在缓冲区中使用指针碰撞分配。
内存分配的线程安全
一种是 CAS + 失败重试进行分配。
另一种是每个线程预先在 Java 堆中分配线程私有的分配缓冲区,Thread Local Allocation Buffer,TLAB,线程分配时先在TLAB中分配,没有足够内存再使用 CAS + 失败重试从堆中分配 TLAB。
-XX:+/-UseTLAB
启用或关闭 TLAB
这好像也是 Java 内存模型的一部分,主要是保证字段即便未显式初始化也可直接使用。
可提前到 TLAB 分配时期就处理。
把内存清零即可。
对象头的初始化,如是哪个类的实例、如何找到类的元数据信息、对象哈希码(Object#hashCode调用时才计算)、GC分代年龄等。
构造函数调用 <init>()
,由 new 指令后是否有 invokespecial 指令决定,java编译器会在new指令后同时生成invokespecial,通过其他方式产生的则不一定。
三部分:对象头
(Header)、实例数据
(InstanceData)、对齐填充
(Padding)。
两部分信息
- 对象自身运行时数据 Mark Word
:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分在32位和64位VM(未开启指针压缩)中分别32位和64位。
- 类型指针:实例的类型元数据指针,VM通过这个指针确定该对象是哪个类的实例。不一定所有实现都需要在对象头保留类型指针。
- 数组长度:如果实例是一个数组的话
Mark Word
动态数据结构,由于运行时数据较多,不同情况下存储的数据不一样。
实例数据
所有字段内容,无论父子类的都需要记录下来(因为最终是一个实例),顺序受VM分配策略(-XX:FieldsAllocationStyle)和源码定义顺序影响。
默认:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)。
默认相同宽度字段一起,这个前提下,父类字段在子类字段前。
若 HotSpot 虚拟机的+XX:CompactFields参数值为true(默认),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。
对其填充
非必然存在,占位符作用,主要是 HotSpot 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也即要求任何对象的大小都必须是8字节的整数倍。
对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍)。
常用方法:句柄、直接指针。
句柄
堆内存划出句柄池,reference 存储的是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自地址信息。
reference 存储稳定句柄地址,若对象移动,仅需要修改句柄池中的数据。
直接指针
reference 存储的直接是对象实例的地址,对象头需要使用类型指针存储对象所属类型的信息。
若只是访问实例,则与句柄相比少了一次间接访问的开销。
Hotspot 使用直接指针的访问方法。