JVM总结之 对象实例化过程 & 内存布局 & 访问定位

1 对象实例化过程

1.1 对象创建的方法 - 了解

  • new:最常见的方式、单例类中调用getInstance的静态类方法、XXXFactory的静态方法
  • Class的newInstance方法:反射的方式,在JDK9里面被标记为过时的方法,因为只能调用空参构造器,并且权限必须为 public
  • Constructor的newInstance(xxxx):反射的方式,可以调用空参或带参的构造器,权限没有要求
  • clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone( )方法
  • 使用反序列化:从文件中、从网络中获取一个对象的二进制流,序列化一般用于Socket的网络传输
  • 第三方库 Objenesis

1.2 创建对象步骤 - 重要

字节码角度分析

public class ObjectTest {
    public static void main(String[] args) {
        Object obj = new Object();
    }
}

main( ) 方法对应的字节码(后面细讲)

  • 调用 new 指令后后,加载 Object 类
  • 调用 Object 类的 init( ) 方法
0: new           #2                  // class java/lang/Object
3: dup # 复制操作
4: invokespecial #1                  // Method java/lang/Object."<init>":()V
7: astore_1
8: return

执行步骤角度(重要)

1)类加载检查

  • 虚拟机遇到一条new指令,首先去检查这个指令的参数 能否在Metaspace(元空间)的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。如果该类没有加载,那么在双亲委派模式下对该类进行加载。

2)分配内存

  • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
  • 内存分配方式有 指针碰撞空闲列表两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

3)初始化零值

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

4)设置对象头

  • 将对象的所属类(即类的元数据信息)对象的HashCode和对象的GC信息锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

5)执行 init 方法

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

内存分配的两种方式:指针碰撞与空闲列表 详解 (重要)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法。

  • “标记-清除”,内存不规整,维护一个空闲列表
  • “标记-整理”(也称作"标记-压缩"),内存规整,使用指针碰撞分类内存
  • 值得注意的是,复制算法内存也是规整的

内存分配中的并发问题(重要)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

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

2 对象的内存布局 重要

对象在JVM中是怎么存储的

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充

对象头信息里面有哪些东西

Hotspot 虚拟机的对象头包括两部分信息

  • 第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等)
  • 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例
|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(64bits)                                               |
|-----------------------------------------------------------------------------------------------------------------|
|                       Mark Word(32bits)                           |  Klass Word(32bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
|     hashcode:25                      | age:4 | biased_lock:0 | 01 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
|     thread:23              | epoch:2 | age:4 | biased_lock:1 | 01 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|     ptr_to_lock_record:30                                    | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|     ptr_to_heavyweight_monitor:30                            | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                              | 11 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|

实例数据

它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)

对其填充

  • 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用
  • 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3 对象的访问定位(句柄 or 直接指针)

JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有 使用句柄直接指针 两种
在这里插入图片描述

  • 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而 句柄中包含了对象实例数据与类型数据各自的具体地址信息
    在这里插入图片描述
    直接指针: 直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。 HotSpot采用的就是这种方式。

在这里插入图片描述
这两种对象访问方式各有优势。

  • 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
  • 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

熠熠98

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值