JVM学习笔记二:HotSpot虚拟机对象

一、对象的创建

 

                         

 

当遇到new指令时,java虚拟机会去检查这个指令的参数能在常量池中定位到一个类的符号引用,同时检查符号引用代表的类是否已被加载、解析、和初始化过。若没有,则先执行相应的类加载过程。

再通过类加载的检查后,虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定。为对象分配空间等同于从Java堆中划分确定大小的内存块出来。

有两种分配方式,选择哪种方式由Java堆内存是否规整决定:

  • 指针碰撞:当Java堆中内存绝对规整时,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。
  • 空闲列表:当Java堆中内存并不规整时,已被使用的内存和空闲的内存相互交错在一起,虚拟机会维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

Java堆是否规整由所在用的垃圾收集器是否带有空间压缩整理的能力决定。

分配空间的同时,还要考虑并发情况下的线程安全问题,有两种解决方案:

  • 对分配内存空间的动作进行同步处理:采用CAS配上失败重试的方法保证更新操作的原子性。
  • 把内存分配的动作按线程划分在不同的空间中进行:即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完 了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来 设定。

分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就能直接使用,使程序能访问到这些字段的数据类型所对应的零值。

Java虚拟机还要对对象的消息头信息进行必要的设置,例如这个对象是哪个类的实例等。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象会有不同的设置方式。

至此从虚拟机的角度来说,一个新的对象已经产生。但从Java程序来说,还需要构造函数,即执行Class文件中的<init>()方法,new指令之后会接着执行<init> ()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

 

二、对象的内存布局

在HotSpot虚拟机中,对象在对内存中的存储布局可以分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

 

1.对象头

对象头包括两类信息:

  • Mark Word
  • Klass Pointer

 

1.1.Mark Word

用于存储对象自身的运行时数据,在64位JVM中的存储内容如下图:

 

32位和64的区别在于长度不一样,基本组成内容是一致的。

  • biased_lock:偏向锁标识,表示是否偏向锁,由于无锁和偏向锁的标志位都是01,为了区分它们,估引入一位的偏向锁标识位;
  • lock:标志位,区分锁状态。当最后两位标志位为11时,对象进入GC回收状态;
  • unused:未使用的;
  • hashCode:特指identity hash code ,表示未经覆盖重写的,由jvm及内存地址计算得出的对象hash值;
  • age:对象分代年龄,表示对象被GC的次数。当次数达到阈值时,对象就会被转移到老年代;age标识位,只有4位,最大能表示15,这也是-XX:MaxTenuringThreshold 这个参数最大只能设置15的原因;
  • thread:偏向线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被设置为该线程的ID。之后就无需再进行获取锁的操作;
  • epoch:偏向时间戳,用于验证偏向锁有效性。偏向锁在CAS锁操作过程中表示对象更偏向哪个锁;
  • pointer_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针;
  • pointer_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针;
  • cms_free:cms收集器有关。未开启指针压缩时,cms会将对象状态标记在klass pointer中,开启指针压缩后空间变小,只能将cms的标示位挪到了mark word中。jdk14 cms已经被移除。

 

1.2.Klass Pointer

类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有虚拟机实现都必须在对象数据上保留指针类型。访问对象的方式有两种,一种是直接指针,另一种则是通过局句柄池访问。

此外如果对象是Java数组,在对象头中还必须存放记录数组长度的数据,否则将无法通过元数据中的信息推断出数组的大小。

 

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字节的整数倍,换句话说就是 任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

 

 

三、对象的访问定位

Java程序会通过栈上的reference数据来操作堆上的具体对象。reference类型只是一个指向对象的应用,对象访问方式由虚拟机实现而定,主流的访问方式主要有句柄直接指针两种;

 

1.句柄访问

使用句柄访问,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址。句柄中包含了对象实例数据与对象类型数据的指针。

  • 优点:reference存的是句柄地址,当对象移动时,只会改句柄中的实例数据指针,不会修改reference。
  • 缺点:多了一次指针定位的时间开销。

 

2.直接指针

使用直接指针访问对象。HotSpot主要使用直接指针进行对象访问。

  • 优点:速度比句柄访问快,因为节省了一次指针定位的时间开销。
  • 缺点:修改reference对象本身。

 

 

 

 

参考资料:

《深入理解Java虚拟机 JVM高级特性与最佳实践(第3版)》

“对象头(object header)”里知多少

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值