JVM对象探秘

   本文以最常用的虚拟机HotSpot与最常用的内存区域java堆为例,深入探讨HotSpot虚拟机在Java对象中分配、布局和访问的全过程。

一、对象的创建

      当Java虚拟机遇到一条字节码new指令时;检测运行时常量池中是否定位到一个类的符号引用及这个符号引用代表的类是否已被加载,解析和初始化过,若否执行相应的类加载过程;若是虚拟机为新生对象分配内存(把一块确定大小的内存从Java堆划分出来)对象所需内存的大小在类加载完成后便可完全确定;堆对象进行必要的设置(对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等);对象创建,执行Class文件中的<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正的对象才算被构造出来了。

       

         步骤2下详解:

         分配内存了,通常有两种方式:指针碰撞与空闲列表

         1使用指针碰撞的前提是堆内存是完全工整的,用过的内存和没用的内存各在一边每次分配的时候只需要将指针向空闲内存一方移动一段和内存大小相等区域即可。如:Serial、ParNew

          2当堆中已经使用的内存和未使用的内存互相交错时,指针碰撞的方式就行不通了,这时就需要采用空闲列表的方式。虚拟机会维护一个空闲的列表,用于记录哪些内存是可以进行分配的,分配时直接从可用内存中直接分配即可。如:CMS

          堆中的内存是否工整是有垃圾收集器来决定的,如果带有压缩功能的垃圾收集器就是采用指针碰撞的方式来进行内存分配的。

          分配内存时也会出现并发问题:这样可以在创建对象的时候使用 CAS 这样的乐观锁来保证。也可以将内存分配安排在每个线程独有的空间进行,每个线程首先在堆内存中分配一小块内存,称为本地分配缓存(TLAB : Thread Local Allocation Buffer)。分配内存时,只需要在自己的分配缓存中分配即可,由于这个内存区域是线程私有的,所以不会出现并发问题。可以使用 -XX:+/-UseTLAB 参数来设定 JVM 是否开启 TLAB 。内存分配之后需要对该对象进行设置,(但不包括对象头)都初始化为零值,保证Java代码中即使不符值就直接使用,访问到这些字段值为0.

二、对象的内存布局

        对象在内存中布局可以分为三个区域:对象头(Header),实例数据( Instance Data)和对齐填充(Padding)

        对象头:

        运行时数据-通过Mark Word实现,包括hashcode、GC分代年龄、锁状态标识、线程持有的锁、偏向锁ID和偏向时间戳,在32位虚拟机中长度为32bit,在64位虚拟机中长度为64bit,非固定的数据结构,以实现在有限空间内保存尽可能多的数据;Mark Word中的数据结构是一直在变化的,根据对象状态的不同,其记录的内容不同,则结构也不同

        实例数据:

         指向它的类元数据的指针,用于判断对象属于哪个类的实例。实例数据存储的是真正有效数据,既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。各字段的分配策略的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。通常情况long/double、int、short/char、byte/boolean、oop(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据,父类定义的变量会出现在子类定义的变量的前面,如果 CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。

         对其填充:

         要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。由于对象头的大小刚好是8bit的整数倍(32bit或者64bit),所以如果实例数据+对象头,不够8字节的整数倍时,需要通过对齐填充进行补全。

三、对象的访问定位

对象的访问定位也取决于具体的虚拟机实现。当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种:

  1. 使用句柄访问对象。java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
  2. 直接指针访问对象。reference直接指向了对象类型数据,那么java堆对象分布中就必须考虑如何放置访问类型数据的相关信息

       这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象呗移动(垃圾收集时移动对象是非常普遍的行为)是只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁。Sun HotSpot虚拟机采用的是第二种方式。下图是句柄访问与指针访问的示意图。

发布了20 篇原创文章 · 获赞 36 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/heijunwei/article/details/104263766