美文网首页
2.HotSpot VM对象探秘

2.HotSpot VM对象探秘

作者: xMustang | 来源:发表于2020-02-22 23:14 被阅读0次

HotSpot VM对象探秘

下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局、访问的全过程。

1. 对象的创建

下面讲述的只是普通的Java对象,不包括数组、Class对象。

先看一张概括过程的图:

Java对象创建过程图示

VM遇到new指令时:

  1. 检查指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始化过。如果没有,先执行相应的类加载过程。类加载过程中,准备阶段给类变量赋默认值,初始化阶段执行<clinit>(),按程序员意图初始化类变量的值。

  2. 为新生对象分配内存,对象所需的内存大小在类加载完成后就可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。

    • 指针碰撞(Bump the Pointer):内存是规整的,用过的内存放在一边,空闲的内存放在另一边。分配时,指针向空闲内存空间移动一段与对象大小相等的距离。
    • 空闲列表(Free List):内存是不规整的,维护一个列表,记录哪些内存块可用,分配时,在列表中找到一块足够大的空间分给对象实例,并更新列表记录。

    选用哪种分配方式由Java堆是否规整决定,Java堆是否规整由所采用的垃圾收集器是否具有压缩整理功能决定:"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。

    在使用Serial、ParNew等带有Compact过程的垃圾收集器时,采用的分配算法是指针碰撞;使用CMS基于Mark-Sweep算法(标记清除算法)的收集器时,分配算法通常是空闲列表。

    并发下给对象分配内存时保证线程安全的方式(同时创建两个对象会出现对对象指针的竞争条件):

    • CAS+失败重试:

      对分配内存空间的动作进行同步处理,实际是采用CAS配上失败重试的方式保证更新操作的原子性。即:每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。

    • TLAB:

      每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),通过 -XX:+/-UseTLAB 设定。

      哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完,并分配新的TLAB时,才需要同步锁定。

  3. 虚拟机将对象分配到的内存空间都初始化为零值(不包括对象头),也就是VM对非静态字段的默认初始化。如果使用TLAB,这一工作可以提前到TLAB分配时进行。

  4. 虚拟机对对象头进行必要的设置。如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。至此,虚拟机视角的new操作完成。

  5. 如果字节码中跟随invokespecial指令,执行<init>方法,按照程序员意愿初始化。首先执行非静态成员变量显式初始化赋值(即private int i = 2;)和初始化代码块按顺序组合成的代码,再执行构造器。(先初始化块,再构造方法)

    如果有继承关系,创建子类对象时,父类的成员变量显式初始化,初始化代码块,构造器都要完成后,再执行子类的成员变量显式初始化,初始化代码块,构造器。

public class Demo {
   public static void main(String[] args) {
      new Father();
   }
}

class Father {
   private int i = 1;

   Father() {
      i = 2;
   }

   {
      i = 3;
   }
}

上面的代码编译成字节码,如下图:

字节码图1 字节码图2
public class Demo {
   public static void main(String[] args) {
      Object obj = new Object();
   }
}

上面的代码编译成字节码:

字节码图3

https://github.com/iMustang/l-jdk/raw/master/src/main/java/basic/constructor/Father.java

public class Father {
    public Father() {
        System.out.println("父类构造器执行");
        i = 3;
    }
    {
        System.out.println("父类初始化块");
        i = 2;
    }
    public int initI() {
        System.out.println("父类成员变量定义时赋值(显式初始化)");
        return 1;
    }
    private int i = initI();
}

public class Son extends Father {
    public Son() {
        System.out.println("子类构造器执行");
        j = 3;
    }
    {
        System.out.println("子类初始化块");
        j = 2;
    }
    private int j = initJ();
    public int initJ() {
        System.out.println("子类成员变量定义时赋值(显式初始化)");
        return 1;
    }
    public static void main(String[] args) {
        new Son();
    }
}

// 执行结果:
父类初始化块
父类成员变量定义时赋值(显式初始化)
父类构造器执行
子类初始化块
子类成员变量定义时赋值(显式初始化)
子类构造器执行

2 对象的内存布局

对象在内存中的布局分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

  1. 对象头

    对象头包含两部分信息:

    • 存储对象自身的运行时数据(官方称为Mark Word),如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

      该部分数据长度为:32位VM为32bit,64位VM(未开启压缩指针)为64bit。

    • 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有VM实现都在对象数据上保留类型指针。

      另外,如果对象是一个数组,在对象头中还有一块用于记录数组长度。

    HotSpot对象头
  2. 实例数据

    记录代码中定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的。存储顺序受到虚拟机分配策略(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。

    HotSpot VM默认的分配策略是longs/doubles、ints、shorts/chars、bytes/boolean、oops(Ordinary Object Pointers),在满足这个前提条件下,父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类中较窄的变量也可能会插入到父类变量的空隙中。

  3. 对齐填充

    并不是必然存在的。HotSpot VM自动内存管理要求对象起始地址是8字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3 对象的访问定位

对象访问方式取决于虚拟机实现,目前主流的访问方式有两种:

  • 使用句柄。Java堆中划分一块内存区域作为句柄池,reference中存储的是对象的句柄地址。句柄中包含了对象的实例数据和类型数据的各自的具体地址信息。
  • 直接指针。reference中存储的直接是对象的地址。HotSpot使用的是这种方式。
使用句柄访问对象 使用直接指针访问对象
  • 使用句柄访问对象最大好处是reference中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,reference本身不需要修改。
  • 使用直接对象指针最大好处是速度更快,节省了一次指针定位的时间开销。

相关文章

网友评论

      本文标题:2.HotSpot VM对象探秘

      本文链接:https://www.haomeiwen.com/subject/kqtdqhtx.html