深入理解java虚拟机(二)
深入探讨Sun HotSpot虚拟机在java堆中对象创建、内存布局和访问定位的全过程。
对象创建
对象创建过程主要包括:类加载检查、为新生对象分配内存、内存空间初始化为零值、设置对象头、执行构造器函数< init >。
类加载检查
首先检查new指令的参数是否能在常量池中定位到一个类的符号用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,则必须先执行相应的类加载过程。
为新生对象分配内存
把一块确定大小的内存从java堆中划分出来。
分配方式:指针碰撞和空闲列表
- 选择依据:分配方式取决于java堆是否规整 ——> java堆是否规整取决于采用的垃圾收集器是否带有压缩整理功能。ParNew、Serial等基于“标记—整理”的垃圾收集器采用指针碰撞,CMS等基于“标记—清除”算法的垃圾收集器采用空闲列表。
- 指针碰撞
如果java堆内存绝对规整,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。分配内存就是将指针向空闲内存那边移动一段与对象大小相等的距离。 - 空闲列表
java堆内存不规整,虚拟机维护一个列表,记录哪些内存块是可用的。分配内存就是从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
并发问题
创建对象行为频繁,线程不安全,解决方案包括同步处理分配内存空间的动作和使用TLAB。
- 同步处理分配内存空间的动作:采用CAS配上失败重试的方式保证更新操作的原子性(乐观锁)
- 使用TLAB:把内存分配动作按照线程划分在不同的空间进行。即,每个线程在java堆中预先分配一小块内存(本地线程分配缓冲TLAB),那个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。通过参数-XX:+/-UseTLAB选择是否使用TLAB。-XX:+/-UseTLAB选择是否使用TLAB。
内存空间初始化为零值
- 如果使用TLAB,可以提前至TLAB分配时进行。
- 这一操作保证对象的实例字段无需显示初始化就可以使用,但是方法的局部变量必须显示初始化才能使用。
设置对象头
- 对象头存放:对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等。
- 根据虚拟机当前的运行状态,对象头设置方式不同。
执行构造器函数< init >
JVM会调用对象的构造函数,调用顺序会一直上溯到Object。
对象的内存布局
对象在内存中的存储布局包括:对象头、实例数据和对齐填充。
对象头(Mark Word和类型指针)
Mark Word
- 这一部分存储对象自身的运行时数据,包括对象的哈希码、GC分代年龄、锁状态标志、线程持有的锁和偏向锁相关。
- Mark Word是一个非固定的数据结构
- 32bit虚拟机—>32bit,64bit虚拟机—>64bit(均未开启压缩指针)
类型指针(非必需)
- 对象指向类元数据的指针,java通过这个指针确定对象是哪个类的实例
- 如果对象是一个java数组,那在对象头中还必须有一块记录数组长度的数据(因为从数组的元数据无法确定数组的大小,而普通java对象可以)
- 32bit虚拟机—>32bit,64bit虚拟机—>64bit(均未开启压缩指针)
实例数据
- 对象真正存储的有效信息
- 存储顺序受虚拟机分配策略参数和字段在java源码中定义顺序的影响。
- 虚拟机默认的分配策略为:longs/doubles、ints、shorts/chars、byte/boolean、oops。即相同宽度的字段总是分配到一起—>父类定义的变量会出现在子类之前
- 通过参数-XX:+/- CompactFields(默认开启),子类中较窄的变量也可能会插入到父类变量的空隙之中
对齐填充
- 占位符作用
- 对象起始地址必须是8字节的整数倍(即对象大小必须是8字节的整数倍)
- 对象头是8字节的1倍或2倍。
对象的访问定位
- 对象的访问定位取决于虚拟机的实现,目前主流的访问方式有:使用句柄和直接指针。
- Sun HotSpot虚拟机采用直接指针访问定位对象。
使用句柄
- java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址。句柄包括了对象实例数据和对象类型数据的地址
- 优点:reference中存储的是稳定的句柄地址
直接指针
- reference中存储的是对象地址
- 优点:速度更快,节省一次指针定位的时间开销,积少成多。
图片来源于网络,侵删