关于后端:深入浅出JVM一之Hotspot虚拟机中的对象

5次阅读

共计 2793 个字符,预计需要花费 7 分钟才能阅读完成。

本篇文章思维导图

对象的创立

对象的创立能够分为五个步骤:查看类加载, 分配内存, 初始化零值, 设置对象头, 执行实例结构器 <init>

类加载查看

  • HotSpot 虚拟机遇到一条 new 指令, 会先查看是否在常量池中定位到这个类的符号援用, 查看这个类是否类加载过

    • 没有类加载过就去类加载
    • 类加载过就进行下一步分配内存

分配内存

对象所需的内存在类加载实现后就能够齐全确定

分配内存形式

虚拟机在堆上为新对象分配内存, 有两种内存调配的形式:指针碰撞, 闲暇列表

  • 指针碰撞

    • 应用场景: 堆内存规整参差
    • 过程: 应用过的空间放在一边, 闲暇的空间放在另一边, 两头有一个指针作为分界点指示器, 把新生对象放在应用过空间的那一边, 两头指针向闲暇空间那边移动一个新生对象的内存大小的间隔即可

特点:简略, 高效 , 因为要堆内存规整参差, 所以垃圾收集器应该要有 压缩整顿 的能力

  • 闲暇列表

    • 应用场景: 已应用空间和闲暇空间交织在一起
    • 过程: 虚拟机保护一个列表, 列表中记录了哪些内存空间可用, 调配时找一块足够大的内存空间划分给新生对象, 而后更新列表
    • 特点: 比指针碰撞简单, 然而对垃圾收集器能够不必压缩整顿的能力
分配内存流程

分配内存流程(栈 – 老年代 –TLAB–Eden)

因为在堆上为对象分配内存, 内存不足会引起 GC, 引起 GC 可能会有 STW(Stop The World)影响响应

为了优化缩小 GC, 当 对象不会产生逃逸 (作用域只在办法中, 不会被外界调用) 且栈内存足够时, 间接在栈上为对象分配内存 , 当线程完结后, 栈空间被回收,(局部变量也被回收) 就不必进行垃圾回收了

开启逃逸剖析 -XX:+DoEscapeAnalysis 满足条件的对象就在栈上分配内存

(当对象满足不会逃逸条件除了可能优化在栈上分配内存还会带来锁打消, 标量替换等优化 …)

  1. 尝试该对象能不能在栈上分配内存
  2. 如果不合乎 1, 且该对象特地的大, 比方内存超过了 JVM 设置的大对象的值就间接在老年代上为它分配内存
  3. 如果这个对象不大, 为了解决并发分配内存, 采纳TLAB 本地线程调配缓冲

TLAB 本地线程调配缓存

堆内存是线程共享的, 并发状况下从堆中划分线程内存不平安, 如果间接加锁会影响并发性能

为每个线程在 Eden 区调配小小一块属于线程的内存, 相似缓冲区

哪个线程要分配内存就在那个线程的缓冲区上调配, 只有缓冲区满了, 不够了才应用乐观的同步策略 (CAS+ 失败重试) 保障分配内存的原子性

在并发状况下分配内存是不平安的 (正在给 A 对象分配内存, 指针还未修改, 应用原来的指针为对象 B 分配内存), 虚拟机 采纳 TLAB(Thread Local Allocation Buffer 本地线程调配缓冲)和 CAS+ 失败重试 来保障线程平安

  • TLAB:为每一个线程事后在伊甸园区(Eden)调配一块内存,JVM 给线程中的对象分配内存时先在 TLAB 调配,直到对象大于 TLAB 中残余的内存或 TLAB 内存已用尽时才须要同步锁定(也就是 CAS+ 失败重试)
  • CAS+ 失败重试:采纳 CAS 配上失败重试的形式保障更新操作的原子性

初始化零值

分配内存实现后, 虚拟机将调配的内存空间初始化为零值(不包含对象头) (零值: int 对应 0 等)

保障了对象的成员字段 (成员变量) 在 Java 代码中不赋初始值就能够应用

设置对象头

把一些信息 (这个对象属于哪个类? 对象哈希码, 对象 GC 分代年龄) 寄存在对象头中 (前面具体阐明对象头)

执行 init 办法

init 办法 = 实例变量赋值 + 实例代码块 + 实例结构器

依照咱们本人的志愿进行初始化

对象的内存布局

对象内存信息

对象在堆中的内存布局能够分为三个局部:对象头, 实例数据, 对齐填充

  • 对象头包含两类信息(8Byte + 4Byte)

    1. Mark Word: 用于存储该对象本身运行时数据 (该对象的 哈希码信息 ,GC 信息: 分代年龄, 锁信息: 状态标记等)
    2. 类型指针 (对象指向它类型元数据的指针):HotSpot 通过类型指针确定该对象是哪个类的实例 ( 如果该对象是数组, 对象头中还必须记录数组的长度)

    类型指针默认是压缩指针, 内存超过 32G 时为了寻址就不能采纳压缩指针了

  • 实例数据是对象真正存储的无效信息

    1. 记录从父类中继承的字段和该类中定义的字段
    2. 父类的字段会呈现在子类字段之前, 默认子类较小的字段能够插入父类字段间的空隙以此来节约空间(+XX:CompactFields)
  • 对齐填充

    HotSpot 要求对象起始地址必须是 8 字节整倍数

    所以 任何对象的大小都必须是 8 字节的整倍, 如果对象实例数据局部未达到 8 字节就会通过对齐填充进行补全

剖析对象占用字节

Object obj = new Object(); 占多少字节?

导入 JOL 依赖

 <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
         <dependency>
             <groupId>org.openjdk.jol</groupId>
             <artifactId>jol-core</artifactId>
             <version>0.12</version>
         </dependency>

mark word : 8 byte

类型指针: 4 byte

对齐填充 12->16 byte

int[] ints = new int[5]; 占多少内存?

mark word:8 byte

类型指针: 4 byte

数组长度: 4 byte

数组内容初始化: 4*5=20byte

对齐填充: 36 -> 40 byte

父类公有字段到底能不能被子类继承?

子类对象的内存空间中保留有父类公有字段,只是无奈应用

栈 - 堆 - 办法区结构图

对象的拜访定位

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

拜访形式

对象实例数据: 对象的无效信息字段等(就是下面说的数据)

对象类型数据: 该对象所属类的类信息(存于办法区中)

  • 句柄拜访
-   在堆中开拓一块内存作为句柄池, 栈中的 **reference 数据存储的是该对象句柄池的地址 **,** 句柄中蕴含了对象实例数据和对象类型数据 **
-   ** 长处: 稳固, 对象被挪动时(压缩或复制算法), 只须要改变该句柄的对象实例数据指针 **
-   ** 毛病: 多一次间接拜访的开销 **
  • 间接指针拜访

栈中的 reference 数据存储堆中该对象的地址 (reference 指向该对象), 然而 对象的内存布局须要保留对象类型数据

长处: 访问速度快

毛病: 不稳固, 对象被挪动时(压缩或复制算法), 须要改变指针

拜访形式是虚拟机来规定的,Hotspot 次要应用间接指针拜访

总结

本篇文章次要从对象的创立流程(类加载、分配内存、初始化零值、设置对象头、执行实例办法)、对象的内存布局(对象头、实例数据、对齐填充)、拜访对象的定位形式(间接指针拜访、句柄拜访)等层面具体介绍了对象,还在其中交叉了栈上调配、TLAB 等内存调配优化以及剖析对象占用具体空间

  • 参考资料

    • 《深刻了解 Java 虚拟机》
    • 局部图片来源于网络

本文由博客一文多发平台 OpenWrite 公布!

正文完
 0