05-JVM对象探秘

一、对象的内存布局

        以Hotspot虚拟机为例,对象在内存中的结构可以分为三部分:对象头(header)、实例数据(instance data)、对齐填充(padding)。

1.1.对象头

        对象头的结构大体相似,但不同JVM的具体实现使得它们略有差别。一般来说,对象头都包含了标记字、类型指针两部分信息,如果对象是数组,还会额外包含数组长度信息。

1.1.1.标记字

        存储对象自身的运行时数据(即状态),包括哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程id、偏向时间戳等。它们的存储结构类似于C语言中的“位字段”,官方称之为“Mark Word(标记字)”。“标记字”以“字”作为基本的存储单元,即在32位虚拟机中,数据长度为32bit;而在64位虚拟机中,数据长度为64bit。
        以32bit虚拟机为例,有固定的2bit用于存储锁标志位,随着锁标志位值不同,其他位存储的内容与位长度也不同。这一点类似于C语言中的联合结构(union),且联合的每一个成员都是位字段结构。

1.1.2.类型指针

        类型指针即对象指向它的类元数据(class metadata)的指针,虚拟机通过该指针确定这个对象是哪个类的实例。但需要注意的是,并非所有虚拟机实现中都会在对象头包含类型指针,也可以采用其他方式保留对象的类型信息

1.1.3.数组长度

        在java中,数组也属于对象,那么理所当然的需要维护数组长度,该信息存放在对象头中。
 

1.2.实例数据

        实例数据即对象的字段(或称为成员变量)存储的数据信息,包含了从父类继承及自己定义的所有字段。且字段在内存中存储的顺序并不等于类中的定义顺序,它受到虚拟机策略的影响(主要考虑到内存对齐以及使用率的问题)。
 

1.3.对齐填充

        类似于C中结构体struct的内存对齐,java对象的内存位置也需要对齐。
        我们常用的Hotspot虚拟机要求每个对象的起始地址为8字节的整数倍,也就是说,若一个对象结束地址非8字节整数倍,则需要占位符进行填充以保证对齐。
 

二、对象的访问定位

        虚拟机规定,需要通过栈上的“reference(引用)”来操作具体对象。对于该规定,目前有两种主流的实现方式:
  • 通过句柄(handler)实现:该种方式会在堆中划出一块“句柄池”内存空间,每个栈上的引用直接指向句柄池中的句柄,而句柄中又会维护对象指针和类型指针。使用句柄带来的好处是,栈上的reference存储稳定的句柄地址,GC造成的对象移动只会导致句柄中相应的指向地址改变,而reference地址不改变。

 

  • 通过直接指针(direct point)实现:即在对象的对象头中维护类型指针。栈的reference指向对象,而对象头中的类型指针指向对象类型数据。使用直接指针的好处是,对象的访问速度快,节省了指针二次寻址的开销。
 
 

三、对象的创建过程

        对象的创建过程要经历以下几个阶段:

3.1.类加载

  • 检查到new指令;
  • 虚拟机检查在常量池中是否有该类的符号引用,包括该符号引用代表的类是否已被加载、解析、初始化;
  • 没有,则先加载类(加载过程后续章节会详细讲述);有的话,直接创建对象;

3.2.内存分配

(1)内存分配的方式
         一个对象所需内存在类加载时便可确定,内存分配方式有两种:
  • 指针碰撞法:若java堆中内存是绝对规整的,所有用过内存都放到一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把指针向空闲空间那边挪动一段与对象大小相等的距离;
  • 空闲列表法:若堆内存不规整,就无法通过简单的移动指针分配内存。这种情况下虚拟机会维护一个列表记录哪些内存可用,分配时查找并更新列表;
        使用哪种方式取决于内存是否规整,而内存是否规整又取决于垃圾收集器的GC算法。典型如serial、parnew这两种垃圾收集器,它们在GC时带有压缩整理的功能,因此系统会采用“指针碰撞”的方式分配内存;而CMS这种基于Mark-Sweep(标记-清除)算法的垃圾收集器,则会采用“空闲列表法”分配内存。
(2)内存分配的安全
        需要注意的是,若多个线程同时申请分配内存,如果不加以同步控制,则会导致内存分配不对。不同的虚拟机会采用不同的机制避免线程安全问题:
  • 同步锁定:通过CAS配上失败重试的方式保证更新操作的原子性。注:CAS,即CPU硬件同步原语,全称为compare and swap(比较并交换),若比较不对则失败;
  • TLAB:即线程分配缓冲区。在堆中预先为每个线程分配一小块内存,线程在各自分配的内存上进行内存分配来保证安全。只有当TLAB用尽并申请新的TLAB时,才进行同步锁定。

3.3.内存初始化

        内存初始化指的是将对象分配到的内存所有位重置为0(不含对象头)。若对象通过TLAB分配的,该过程会提前至“内存分配”执行.

3.4.对象头初始化

        设置对象的对象头信息。

3.5.对象实例数据初始化

        设置对象的实例数据信息,即成员变量值。只有这步完成了,一个真正的对象才产生并能提供给我们使用。
posted @ 2018-12-15 16:34  re-phoenix  阅读(608)  评论(0编辑  收藏  举报
levels of contents