对象
Hotspot虚拟机在Java堆中对象分配、布局和访问的全过程。
对象的创建
1.类加载
当Java虚拟机遇到new
指令时,就会去常量池中查找是否存在该对象的类的引用,检查类是否已经被加载、解析和初始化过。如果没有,就会执行类加载的过程。
2.内存分配
对象所需要的内存大小在类加载完成后就确定下来了。对象内存分配是从java堆中划分一块空间给对象。
内存的分配方式
- 指针碰撞(
Bump The Pointer
):如果Java堆中,已经使用的内存在一边,空闲的内存在另一边,即Java堆中的内存是十分规整的,则通过一个指针指向两者的分界处,当完成内存分配后,指针往空闲空间那边移动对应的距离。 - 空闲列表(
Free List
):通过一个列表来维护空闲的内存空间。
具体的分配方式如上所述,其实是取决于内存空间是否是规整的,内存空间是否规整那就取决于垃圾回收算法了。
线程安全问题
对象的创建十分频繁,指针的移动如何保证线程安全问题。
- 同步处理:对内存空间的分配进行同步处理,HotSpot虚拟机底层使用的是CAS和失败重试来保证原子性。
- 本地线程缓冲(
Thread Local Allocation Buffer, TLAB
):给每个线程预先分配一小块内存,用完了在通过同步操作来获取。通过配置参数-XX:+/-UseTLAB
来设定。
3.初始化
完成内存分配后,虚拟机会对对象的所有属性初始化为零值。这估计也是Java对象的属性字段不用初始化就能使用的原因。
4.对象的信息标记
这一步就相当于确定对象的身份。包括这个对象属于那个类、类的元数据信息、对象的HashCode、对象的GC年龄。而这些信息被放入了对象头(Object Header
)中,根据当前状态不同,会有不同的设置。
4.构造器
执行init方法,调用构造器来创建对象。
对象的内存布局
对象由三部分组成:对象头(Object Header
)、实例数据(Instance Data
)、对齐填充(Padding
)。
1.对象头
对象头包括两类信息:
- 自己从哪来:类型指针,这个指针指向了对象的类型元数据,就是我属于那个类的实例。
- 运行时所需要的数据:哈希码、GC年龄、锁状态标志、线程持有的锁、偏向线程ID等。这部分长度在32位和64位虚拟机中分别为32bit和64bit,称为**
Mark Word
**。Mark Word
被设计成动态定义的数据结构,在极小的空间存储更多的数据。使用对象当前的状态来复用存储空间。例如,在32位虚拟机中,Mark Word
大小位32bit,25个bit来存储对象的hashCode,4个bit来存储GC分代年龄,2个bit来存储锁的标志位,还有1个bit固定为0。
2.实例数据
存储的是真正有效的数据信息,我们在对象中定义的各种类型的字段。这部分的存储顺序会 受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。 HotSpot虚拟机默认的分配顺序为longs/doubles
、ints
、shorts/chars
、bytes/booleans
、oops(Ordinary Object Pointers,OOPs)
,从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存 放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的 +XX:CompactFields
参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空 隙之中,以节省出一点点空间。
3.对齐填充
因为HotSpot虚拟机要求对象的起始内存地址必须是8的整数倍,所以当对象的实例数据没有对齐的话,就会填充来保证。
对象的访问定位
Java程序通过栈上的Reference
来操作对象的具体对象。访问具体对象的方式有两种:
- 句柄池:堆中开辟了一块空间来维护句柄,句柄存储了具体对象的地址。
Reference
通过访问句柄池来获取具体对象的地址。 - 直接指针:
Reference
存储的是对象的地址,这种方式不需要中转。
优点对比
分析来看,在垃圾回收场景中,对象的地址可能会变,前者Reference
存储的是句柄地址,会更加稳定。后者不需要中转速度更快。HotSpot使用的是后者。
OOM异常模拟试过了就不记录了。。。。。。。。。。。。。。。。。。。。。。。。