前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM 中对象咋创建啊,又怎么访问啊

JVM 中对象咋创建啊,又怎么访问啊

作者头像
星尘的一个朋友
发布2020-12-30 16:23:57
5350
发布2020-12-30 16:23:57
举报
对象的创建与访问
对象的创建与访问

JVM 中对象咋创建啊,又怎么访问啊

虚拟机遇到 new 指令,会根据指令参数去常量池找对应类的符号引用,如果没找到会进行类加载,此时会执行类构造器指令。类加载完成之后,初始化之前,开始进行对象内存分配,分配好之后将内存区域的值全部置为0(成员变量初始化),之后执行实例构造器指令 ,完成后返回对象引用。

目录:

  1. 对象是怎么完成创建的?
  2. 怎么分配内存?
  3. 对象在内存中都存了什么?
  4. 怎么在内存中定位访问一个对象?

对象是怎么完成创建的?

对象的创建一共有四种方式

  • new 关键字
  • 复制(clone操作)
  • 序列化(另类操作)
  • 反射(另类)
※new 关键字创建普通 java 对象的过程
  1. 在常量池中查找类信息(根据全部限定名),如果没有先进行类加载(后面在虚拟机执行章节中有具体的加载过程笔记),然后检验其是否被初始化(这个初始化是指的类初始化,也就是执行)过
  2. 类加载完成确定类的内存大小
  3. 在新生代分配内存
  4. 执行构造函数,返回引用地址

简单总结:类初始化 - 分配内存 - 实例初始化 - 返回引用地址

多学一点,这里的几个步骤涉及多个指令操作,所以就有了 DCL 单例使用 volatile 来禁止指令重排来保证单例模式的实例同步

class 文件中的 static 关键字修饰的方法或变量成为类变量,没有被 static 修饰的部分称为实例变量

下面是对象创建细节的拆分

怎么分配内存
  • 指针碰撞 如果内存中现有的分配情况为整齐分布,则会有一个 分界点指示器已用内存未用内存 之间。对于这种情况,只需要将该指示器的位置向后移动当前对象的内存大小位置即可。
  • 空闲列表 更多情况下,内存的使用是不连续的,所以在 JVM 中有一个对于当前内存情况管理的一个列表,称为 空闲列表 ,可以通过查询该表来完成对象的内存的分配。

使用 指针碰撞 的前提是堆内存空间完整,而内存空间完整的前提是垃圾收集器是否有空间压缩整理能力。

SerialParNew 垃圾回收器是带有压缩整理能力的,其可以使用指针碰撞的分配方式

CMS 是不具有压缩整理能力的,所以其使用的是空闲列表方式,但在 CMS 垃圾回收器中,它仍然可以使用类似 指针碰撞 的功能,其在空闲列表中申请内存时会申请较大的一块区域,然后对这块区域是 指针碰撞 来分配。

注:指针碰撞在极客时间郑雨迪的《深入拆解Java虚拟机》中翻译成指针加法

我猜测会有留言问为什么不把 bump the pointer 翻译成指针碰撞。这里先解释一下,在英语中我们通常省略了 bump up the pointer 中的 up。在这个上下文中 bump 的含义应为“提高”。另外一个例子是当我们发布软件的新版本时,也会说 bump the version number。

内存分配的并发问题

由于多线程情况,有可能刚申请的内存被其他线程提前写入,导致内存分配出现问题。所以 JVM 提出了两种解决方案。

  1. 使用 CAS + 失败重试;
  2. 为本地线程分配缓冲区 TLAB (通过参数可选 -Xx : - UseTLAB)缓冲区用完之后在使用 CAS + 失败重试分配内存;

TLAB : 线程需要维护两个指针(实际上可能更多,但重要也就两个),一个指向 TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾。接下来的 new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB 已经没有足够的空间来满足本次新建操作。这个时候,便需要当前线程重新申请新的 TLAB。

使用内存

内存分配完之后, JVM 会将这部分区域的值置为0(这就是基本数据类型的默认值的实现),如果使用的是本地线程缓冲区的方案,在分配缓冲区时即已经置为了0,然后开始设置对象头的信息,包括类信息、元数据地址、GC分代年龄、偏向锁等信息,其中哈希值延迟到调用时才会计算并设置。

至此对象在内存中"完成创建",但此时的对象并不能使用,接着会继续执行构造函数中的内容,来完成对象程序中的初始化步骤,构造函数执行结束后,对象完成创建。

注:以上对象创建过程代码在 hotspot 虚拟机 bytecodeInterpreter.cpp line:2179

对象在内存中都存了什么?

  • 对象头
  • 实例数据
  • 对齐填充
对象头
  1. MarkWord —— 对象自身运行时数据
  2. 类型指针 —— 对象的类型元数据指针
  3. 数组长度 —— 如果是数组对象的话

MarkWord

  • 哈希码
  • GC分代年龄
  • 锁标志
  • 线程持有的锁
  • 偏向锁持有线程ID
  • 偏向时间戳

存储内容

锁标志

状态

哈希码、分代年龄

01

未锁定

指向锁记录的指针

00

轻量级锁

指向重量级锁的指针

10

重量级锁

11

GC标记

持有偏向锁的线程ID、时间戳

01

偏向锁

类型指针

JVM 通过类型指针来确定当前对象的类型。这个类型指针指向方法区中该对象的元空间数据。

数组长度

之所以会单独区分出数组的长度信息,是因为 JVM 无法通过类的元空间数据得出对象的大小,所以单独记录数组对象的长度信息在对象头中。

HotSpot虚拟机代表Mark Word中的代码(markOop.cpp)注释片段,它描述了32位虚拟机MarkWord的存储布局:

实例数据

无论是从父类继承下来的,还是在子类中定义的字段存储顺序会受到虚拟机分配策略参数

(-XX:FieldsAllocationStyle参数) 和字段在Java源码中定义顺序的影响。

HotSpot虚拟机默认的分配顺序为

  1. longs/doubles
  2. ints
  3. shorts/chars
  4. bytes/booleans
  5. oops(Ordinary Object Pointers,OOPs)

从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。

如果HotSpot虚拟机的 +XX:CompactFields 参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。

对齐填充

hotspot 实现的虚拟机,对对象的起始地址有要求,需要是8字节的整数倍,所以对象的大小就必须是8字节的整数倍,如果不足便需要通过占位符来补充至8字节的倍数。

怎么在内存中定位访问一个对象?

Java 程序通过栈上的 reference 数据来操作堆上的对象。

《Java虚拟机规范》没有说明和约束 reference 的实现方式,所以具体的实现由虚拟机决定。

通常由下面两种方式实现

句柄

句柄保存在句柄池中

句柄保存对象数据的地址和对象类型信息的地址,多进行一次操作。但在 GC 做标记-整理操作时,无需关心对象内存地址的信息变化。

直接指针

保存对象的数据信息和对象类型信息的地址,可以直接访问到对象数据。当需要使用类信息的时候,需要在进行一次查找。

图片来自《深入理解 Java 虚拟机》(第三版)周志明

句柄
句柄
直接指针
直接指针
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-12-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JVM 中对象咋创建啊,又怎么访问啊
  • 目录:
    • 对象是怎么完成创建的?
      • 对象在内存中都存了什么?
        • 怎么在内存中定位访问一个对象?
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档