JVM - 探索HotSpot虚拟机中的对象

深藏阁楼爱情的钟 2023-05-30 13:56 79阅读 0赞

文章目录

  • JVM - 探索HotSpot虚拟机中的对象
      1. 我们所熟悉的对象是如何创建的?
      • 1.1 类加载检查
      • 1.2 分配内存
        • 1.2.1 指针碰撞
        • 1.2.2 空闲列表
        • 1.2.3 内存分配方式总结以及如何解决并发问题
      • 1.3 初始化零值
      • 1.4 设置对象头
      • 1.5 执行 init 方法
      1. 对象的内存结构
      1. 我们是如何访问对象的?
      • 3.1 句柄方式
      • 3.2 直接访问方式
      • 3.3 两种访问方式比较

JVM - 探索HotSpot虚拟机中的对象

1. 我们所熟悉的对象是如何创建的?

 在前面的文章中我们已经大概了解了虚拟机的各个结构,这里我们就来详细去探索一下我们所熟知的对象在HotSpot 虚拟机的堆中是如何进行分配、布局和访问的。

 我们每天每时每刻都在不停地new各种各样的对象,毫不夸张地说我们都已经形成了肌肉记忆了。但是我们new的每一个对象,他们都是怎样去创建的呢?我们先看一段简单的代码慢慢入局。

  1. public class User {
  2. static {
  3. System.out.println("static");
  4. }
  5. private String name;
  6. private int age;
  7. public User() {
  8. System.out.println("constructor name:" + this.getName() + " age:" + this.getAge());
  9. }
  10. public String getName() {
  11. return name;
  12. }
  13. public void setName(String name) {
  14. this.name = name;
  15. }
  16. public int getAge() {
  17. return age;
  18. }
  19. public void setAge(int age) {
  20. this.age = age;
  21. }
  22. public static void main(String[] args){
  23. User user = new User();
  24. }
  25. }
  26. static
  27. constructor name:null age:0

 这是一个很简单的对象的创建,从上面的结果我们可以得出两个很明显的结论:

  1. 静态方法在构造方法前执行
  2. 基本数据类型会根据数据类型初始化对应的零值,而引用类型会默认赋值null


 这其中的奥秘就是我们接下来要探索的对象在虚拟机中的创建过程。对象的创建过程主要就是以下五个步骤,我们先做大概了解之后再各个击破。

在这里插入图片描述

1.1 类加载检查

 我们每次new对象的时候,虚拟机就会接收到一条对应的指令。接收指令之后,它就会去检查通过该指令的参数是否能在常量池中定位到这个类的符号引用,并检查该符号引用代表的类是否已被加载、解析和初始化。若没有,那就会先执行相应的类加载过程。

1.2 分配内存

 当类加载检查通过后,接下来虚拟机将为新生对象去分配内存空间。每个对象所需的内存空间大小在类加载完成后便已确定,给对象分配内存空间其实就是从Java堆中将一块已知大小的内存空间划分出来。

 Java堆分配方式主要有指针碰撞空闲列表两种,选择哪一种分配方式则由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

1.2.1 指针碰撞

 指针碰撞方式原理就是将已使用和空闲的内存空间单独分开,使用一个分界指针去分隔两块内存。
在这里插入图片描述
 当我们申请新的内存空间时,只需将指针向空闲内存空间移动指定内存空间大小的位置即可。
在这里插入图片描述
 这种内存分配方法有一个很大的局限性就是内存的分配需要是整齐有规律的,不能有大量的内存碎片,否则浪费极大的内存空间。

1.2.2 空闲列表

 既然指针碰撞的分配方式有局限性,那必然构思一种其他的分配方式弥补其不足。空闲列表的内存分配方式就出现了。
在这里插入图片描述
 这种方式主要就是用于内存碎片多、堆内存不规整的情况。虚拟机通过维护一个列表去记录空闲内存块的相关信息。当我们需要分配一块新的内存时,会通过记录表中的信息挑选一个足够大小空间的内存指定给对象实例。

1.2.3 内存分配方式总结以及如何解决并发问题

指针碰撞

  1. 适用场景:堆内存规整(没有内存碎片)
  2. 原理:已使用和未使用的内存单独放在两边,中间使用一个分界指针隔开,只需将指针向未使用内存方向移动申请新的内存空间大小的位置即可
  3. 对应GC收集器:Serial、ParNew

空闲列表

  1. 适用场景:堆内存不规整
  2. 原理:虚拟机会维护一个列表用于记录空闲内存块的相关信息,分配内存时会指定一块足够大小空间的内存给对象实例并更新记录表信息
  3. 对应GC收集器:CMS

内存分配并发问题

 在我们实际开发中,创建对象是一个十分频繁的行为,所以其实在这个过程中是存在线程安全问题的。第一是因为无论哪种内存分配方式其实都不是一气呵成的,例如开辟内存和移动指针、修改空闲列表都是独立单元的行为,第二是因为在同一时间很可能存在多个对象都进行内存分配。

 作为一个优秀的虚拟机,是早就考虑到了这一点的,为了保证线程安全主要采用两种方式:

  1. CAS+失败重试: CAS是乐观锁的一种实现方式。所谓乐观锁就是每次不加锁,而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  2. TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

1.3 初始化零值

 当内存分配完成后,虚拟机会将分配到的内存空间都初始化为零值,这是为了保证对象的实例属性在 代码中不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。

1.4 设置对象头

 初始化零值完成后,虚拟机会给对象实例进设置对象头相关数据:GC分代年龄、对象哈希吗(HashCode)、元数据信息等。

1.5 执行 init 方法

 当我们前面几步都完成了之后,对于虚拟机来说其实新对象的创建已经完成了,但是对于程序的角度来说对象实例的构建才刚刚开始。一般来说,new指令执行后会紧接着执行对象的init方法,例如对象的构造方法。通过init方法将对象按照我们的需要进行初始化,最终一个真正可用的对象才算创建完成。

2. 对象的内存结构

 上面我们知道了对象是怎么在内存中进行分配的,这里我们再来看看对象在内存中的结构具体是怎么样的。在Hotspot虚拟机中,对象在内存中的结构可以简单分为3部分:对象头实例数据对齐填充

对象头

 对象头主要用于存储对象的元数据信息,这其中又可以分为两部分:

  1. Mark Word:这一部分的数据长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit,存储对象自身的运行时数据如哈希码、GC分代年龄、锁状态等。Mark Word一般被设计为非固定的数据结构,以便存储更多的数据信息和复用自己的存储空间。
  2. 类型指针:指向它的类元数据的指针,用于判断该对象属于哪个类的实例。

实例数据

 实例数据部分存储的是真正有效数据,也就是我们程序中定义的各种类型的字段内容。各字段的分配策略为longs/doublesintsshorts/charsbytes/booleanoops(ordinary object pointers),为了方便读取数据,相同宽度的字段总是被分配到一起。

对齐填充

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

3. 我们是如何访问对象的?

 我们创建了对象实例之后,Java程序就是通过虚拟机栈上的reference类型数据来操作堆上具体某个对象的。对象的访问方式由虚拟机实现决定,目前主流的访问方式有两种:

  1. 使用句柄访问
  2. 直接指针访问(HotSpot虚拟机采用该方式访问对象实例)

3.1 句柄方式

 使用句柄方式访问对象实例,Java堆就会划分一块内存空间作为句柄池。即reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针。
在这里插入图片描述

3.2 直接访问方式

 如果使用直接指针访问对象实例,那么reference中存储的就是对象的地址,相当于一级指针。
在这里插入图片描述

3.3 两种访问方式比较

 既然同时存在两种访问方式,那肯定是各有优劣,这里主要从两个方面去比较:

  1. 垃圾回收:站在垃圾回收角度进行分析,当垃圾回收移动对象时,使用句柄访问方式由于reference中存储的是稳定的句柄地址,仅需要修改句柄中实例数据指针,reference不需要改变。而直接访问指针方式,则需要修改reference中存储的地址。
  2. 访问效率:使用句柄访问需要进行两次指针定位,而直接访问方式仅需要进行一次指针的定位,节省了时间开销,访问的效率更快。

发表评论

表情:
评论列表 (有 0 条评论,79人围观)

还没有评论,来说两句吧...

相关阅读

    相关 HotSpot虚拟对象

    HotSpot虚拟机对象 HotSpot虚拟机在Java堆中对象分配、布局和访问的过程 1、对象创建 \--①虚拟机遇到一个new指令时,首先将去检查这个 指令的参数 是

    相关 HotSpot虚拟对象

    对象的创建   Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。 在语言层面上,创建对象(例如克隆、 反序列化)通常仅仅是一个new关键

    相关 HotSpot虚拟对象详解

    1. 对象的创建:当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用是否已被加载、解析和初始化过,如果没有则会