JVM-JVM对象探秘

对象的创建

对象创建过程.png

类加载检验

  虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

分配内存

  • 指针碰撞
    适用场合:堆内存规整(没有内存碎片)的情况
    原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将改指针移动对象内存大小位置即可。
    GC 收集器:Serial、ParNew
  • 空闲列表
    适用场合:堆内存不规整的情况
    原理:虚拟机会维护一个列表,该列表中会记录哪些内存模块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
    GC 收集器:CMS

  • 如何解决内存分配的并发问题?
      在并发情况下,虚拟机分配内存不是线程安全的,可能出现正在给 A 分配内存,指针还没来的及修改,对象 B 又同时使用了原来的指针来分配内存的情况。

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

初始化零值

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

设置对象头

  初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头(Object Header)中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

执行 init 方法

  在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,init 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 init 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

  在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)实例数据(Instantce Data)对齐填充(Padding)

对象头(Header)

  对象头(非数组对象)包括两部分:

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。

  • 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

  • 如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。

  在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0


32位无锁状态.png

  在 32 位系统下,存放 Class 指针的空间大小是 4 字节,Mark Word 空间大小也是4字节,因此头部就是 8 字节,如果是数组就需要再加 4 字节表示数组的长度


32位有锁状态.png

在 64 位系统及 64 位 JVM 下,开启指针压缩,那么头部存放 Class 指针的空间大小还是4字节,而 Mark Word 区域会变大,变成 8 字节,也就是头部最少为 12 字节


64位.png

压缩指针:开启指针压缩使用算法开销带来内存节约,Java 对象都是以 8 字节对齐的,也就是以 8 字节为内存访问的基本单元,那么在地理处理上,就有 3 个位是空闲的,这 3 个位可以用来虚拟,利用 32 位的地址指针原本最多只能寻址 4GB,但是加上 3 个位的 8 种内部运算,就可以变化出 32GB 的寻址。

实例数据(Instantce Data)

  实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类定义的,都要记录下来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响。

对齐填充(Padding)

  对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的作用。
  由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,也就是说对象的大小必须是 8 字节的整数倍。对象头部分是 8 字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问

  Java中的对象在堆内存中分配内存空间,引用保存在栈内存中,通过引用定位对象的具体位置通常有两种方式:

句柄访问

句柄访问.jpeg

  此方式在堆空间维护一个句柄池,对象引用中保存的是对象的句柄位置。句柄中包含各对象的实例数据和类型数据的地址信息。
  此方式的好处是对象引用中保存的是稳定的对象句柄的地址,因为对象的移动在GC过程中是非常普遍的行为,对象的移动会导致实例数据的地址发生变化。带来的缺点就是访问效率受影响。

直接指针访问

直接指针访问.png

  对象引用中保存的直接的对象地址。该方式的优点是节省了一次指针定位的开销,访问速度快。缺点是当对象地址发生变化是引用中保存的数据也需要变化。

你可能感兴趣的:(JVM-JVM对象探秘)