对象的创建

Java 是一门面向对象的编程语言,创建对象通常只是通过 new关键字创建。

对象创建过程

当虚拟机遇到一个字节码 new指令的时候,首先去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用。并且检查这个符号引用代表的类是否被虚拟机类加载器加载。如果没有,必须先执行类加载的流程。
![image.png](https://img-blog.csdnimg.cn/img_convert/e23c417500168e8b854cce66746d3dec.png#clientId=u80b999e5-1f11-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=250&id=ub4efd892&margin=[object Object]&name=image.png&originHeight=250&originWidth=607&originalType=binary&ratio=1&rotation=0&showTitle=true&size=25526&status=done&style=none&taskId=u235021f7-ad34-4901-b716-637ff406103&title=类的生命周期&width=607 “类的生命周期”)
在类的检查通过过后,接下来虚拟机就会为新生成对象分配内存。对象所需要的内存大小在类加载的时候决定。(对象内存分配后面将有独立的一小段讲解)。

内存分配完成后,虚拟机会将这块分配到的内存空间(不包括对象头)都初始化为零值,就是将这块内存空间进清理和初始化。

接下来虚拟机还需要进行对象进行初始化设置,比如元数据(对象是那个类的实例)、对象的哈希编码、对象的 GC 分代年龄、偏向锁状态等信息这些信息都用于存放到对象头(Object Header)中。

完成上述流程,其实已经完成了虚拟机中内存的创建,但是我们在 Java 执行 new创建对象的角度才刚刚开始,我们还需要调用构造方法初始化对象(可能还需要在此前后调用父类的构造方法、初始化块等)。进行 Java 对象的初始化。即在 .class 的角度是调用 <init>()方法。如果构造方法中还有调用别的方法,那么别的方法也会被执行,当构造方法内的所有关联的方法都执行完毕后,才真正算是完成了 Java 对象的创建。

整体对象创建流程如下:
![image.png](https://img-blog.csdnimg.cn/img_convert/51d3f337dafd10d1a6e7924bd4c32dc4.png#clientId=u2b8aadb8-b689-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=540&id=uf0463cf8&margin=[object Object]&name=image.png&originHeight=540&originWidth=427&originalType=binary&ratio=1&rotation=0&showTitle=true&size=26958&status=done&style=none&taskId=u5765dbde-db20-4d05-9242-92d161f6865&title=对象创建流程&width=427 “对象创建流程”)

对象内存分配

对象内存分配过程如下图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/6860b1cd91dafa5273c8ca229b3dfb9a.png#clientId=u80b999e5-1f11-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=445&id=bWVxT&margin=[object Object]&name=image.png&originHeight=445&originWidth=1144&originalType=binary&ratio=1&rotation=0&showTitle=true&size=50304&status=done&style=none&taskId=u03f730b0-afa7-4224-9500-665a289fcdf&title=对象内存分配过程&width=1144 “对象内存分配过程”)
为对象分配空间的任务实质上是从 Jvm 的内存区域中,指定一块确定大小的内存快给 Java 对象。(默认是在堆上分配)。

指针碰撞

假设 Java 堆中内存是绝对规整的,所有使用过的内存都被放在一边,没有使用过的内存放在了另外一边。中间放着一个指针用来表示他们的分界点。那所分配的内存仅仅是把那个指针向空闲的方向挪动一段与Java对象大小相等的距离,这种分配方式叫做**“指针碰撞”(Dump The Pointer).**

| | 已 | 使 | 用 |
|
|
|
|
|
|
| — | — | — | — | — | — | — | — | — | — |
| | | | |
|
|
|
|
|
|
| | 未 | 使 | 用 | | | | | | |
| | | | | | | | | | |

空闲列表

但是如果 Java 堆中内存并不是规整的,已经使用的内存块,和空闲的内存块相互交错在一起,那就没有办法简单的进行指针碰撞了,虚拟机必须维护一个可用内存区域列表。记录那些内存块是可以使用的。在对象内存分配的时候就从列表中去找到一块足够大的内存空间划分给实例对象,并且更新列表上的记录。这种分配方式叫做“空闲列表”(Free List).

| | 已 | 使 | 用 |
|
|
|
|
|
|
| — | — | — | — | — | — | — | — | — | — |
| | | | |
|
|
|
|
|
|
| | 未 | 使 | 用 | | | | | | |
| | | | | | |
| | | |

内存分配方式选择

什么时候使用指针碰撞,什么时候才用空闲列表?
选择哪一种分配方式是由 Java 堆是否规整决定的,而 Java 堆是否规整又是由所采用的垃圾回收器是否有空间整理(Compact)的能力决定。

  • 当使用 Serial 、ParNew 等带指针压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单,又高效。
  • 当采用 CMS 基于清除(Sweep)算法的收集器时,理论上只能采用复杂的空闲列表来分配内存。

并发内存分配方案

对象频繁分配的过程中,即使只修改一个指针所指向的位置,但是在并发的情况下也不是线程安全的,可能出现
正在给 A 对象分配内存,指针还没有来得及修改,对象 B 又同时使用来原来的指针进行内分配的情况。解决这个问题有两种可选的方案:
一种是对内存分配空间的动作进行同步处理-实际上虚拟机是采用** CAS + 失败重试的方式来保证更新操作的原子性。
另外一种就是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在 Java 堆中预先分配一小块内存,称为
本地线程分配缓冲(Thred Local Allocation Buffer, TLAB)**, 那个线程要分配内存,就在那个线程分配内存,就在那个线程的本地缓冲中分配,只有本地缓冲用完了,分配新的缓冲区时才需要同步锁定,虚拟机是否使用 TLAB,可以通过 -XX:+/-UseTLAB参数设置。

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构:
![对象内存结构 (1).png](https://img-blog.csdnimg.cn/img_convert/469bfb7b039bb89a6d9d3543f67d1bf5.png#clientId=ua70b3623-b62e-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=369&id=u05c63e74&margin=[object Object]&name=对象内存结构 (1).png&originHeight=1114&originWidth=1656&originalType=binary&ratio=1&rotation=0&showTitle=true&size=140741&status=done&style=none&taskId=uba230437-a207-4dbf-adea-e3de5fe1ec9&title=Java 对象结构&width=549 “Java 对象结构”)

对象头结构

Mark Word (64bit)

结合 openjdk 源码 markOop.hpp中我们可以看到

| 锁状态 | 56bit | | 1bit | 4bit | 1bit
是否偏向锁 | 2bit
锁标志位 |
| — | — | — | — | — | — | — |
| 无锁 | unused:25bit | 对象 hashCode: 31bit | unused | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程id: 54bit | Epoch: 2bit | unused | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针(ptr_to_lock_record) | | | | | 00 |
| 重量级锁 | 指向互斥锁(重量级锁)的指针(ptr_to_heavyweight_monitor) | | | | | 10 |
| GC 标记 | 空 | | | | | 11 |

两个指针变量说明:

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM 使用原子操作而不是 OS 互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM 通过 CAS 操作在对象的 Mark Word 中设置指向锁记录的指针。

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor 的指针。 如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到 Monitor 以管理等待的线程。在重量级锁定的情况下,JVM 在对象的 ptr_to_heavyweight_monitor 设置指向 Monitor 的指针。

markOop.hpp中我们可以看到 文件的注释如下

// 部分省略
//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
// 部分省略

klass

klass 对应 Java 的 CLass 类,一个对象 jvm 中就会生成一个 kclass 实例对象存储到 Java 类对象的元数据信息,在 jdk 1.8 中,将这块存储到元空间中。
在对象投中存储的就是 klass 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.

数组长度(只有数组对象有)

如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段、方法内容。无论是从父类继承下来的,还是在子类中定义的,都在这里一一记录。

对齐填充

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

对象大小计算

  1. 在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
  2. 在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
  3. 64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。 数组长度4字节+数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+数组markword为4字节(64位未开启指针压缩的为8字节))+对齐4=16字节。
  4. 静态属性不算在对象大小内。

打印对象状态

JOL(Java Object Layout)一款开源的用于分析 JVM 中对象布局的一个小工具。使用 Unsafe、JVMTI 和 Serviceability Agent (SA) 来解码实际的对象布局、占用空间和引用。这使得 JOL 比其他依赖堆转储、规范假设等的工具更准确。
maven 仓库依赖如下:

<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version>
</dependency>

1、查看对象内部信息包括:对象内的字段布局、标题信息、字段值、对齐/填充。 ClassLayout.parseInstance(obj).toPrintable()
![image.png](https://img-blog.csdnimg.cn/img_convert/c5fcd5ebbaeb21d3b62c4baa85c22e99.png#clientId=u80b999e5-1f11-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=189&id=ua62b6b1b&margin=[object Object]&name=image.png&originHeight=189&originWidth=1032&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36966&status=done&style=none&taskId=u2d113145-2ee4-49e6-8b03-1a1abfccedf&title=&width=1032)
2、查看对象外部信息:包括引用的 :GraphLayout.parseInstance(obj).toPrintable()
![image.png](https://img-blog.csdnimg.cn/img_convert/51791f1ba3047880bffa4c5520ffe443.png#clientId=u80b999e5-1f11-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=159&id=u02566f7b&margin=[object Object]&name=image.png&originHeight=159&originWidth=753&originalType=binary&ratio=1&rotation=0&showTitle=false&size=19228&status=done&style=none&taskId=u929725c3-4b43-4bb0-8493-7a999ea47ca&title=&width=753)
3、查看对象占用内存空间的大小:GraphLayout.parseInstance(obj).totalSize()

16

完整代码:

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;public class ObjectTest2 {public static void main(String[] args) {Object obj = new Object();System.out.println(ClassLayout.parseInstance(obj).toPrintable());System.out.println();System.out.println();System.out.println(GraphLayout.parseInstance(obj).toPrintable());System.out.println();System.out.println();System.out.println(GraphLayout.parseInstance(obj).totalSize());}
}

对象的访问定位

句柄访问

使用句柄访问方式,Java堆中将可能会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/7705917b7830d44e4122e536bb706e3b.png#clientId=ue57d04b7-f3ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=420&id=u62a6c12f&margin=[object Object]&name=image.png&originHeight=473&originWidth=1007&originalType=binary&ratio=1&rotation=0&showTitle=true&size=158955&status=done&style=none&taskId=ubfc26359-403d-4ecb-a037-dc5de516de1&title=通过句柄方式访问对象&width=895.1111111111111 “通过句柄方式访问对象”)

直接访问

直接指针访问,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图下图所示:
![image.png](https://img-blog.csdnimg.cn/img_convert/56683d68fe3ca897b5780de5171670da.png#clientId=ue57d04b7-f3ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=411&id=ua79074e6&margin=[object Object]&name=image.png&originHeight=462&originWidth=1005&originalType=binary&ratio=1&rotation=0&showTitle=true&size=135849&status=done&style=none&taskId=u93be3445-de47-4dd2-b141-a24b93b280a&title=直接指针访问对象&width=893.3333333333334 “直接指针访问对象”)

对象访问方式对比

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。

参考资料

  • 《深入理解 JVM 虚拟机 第三版》周志明
  • https://www.cnblogs.com/jhxxb/p/10983788.html
  • https://www.cnblogs.com/maxigang/p/9040088.html
  • https://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf
  • https://github.com/openjdk/jol

基于 Hostpot 虚拟机的 Java 对象解析相关推荐

  1. 一文看懂虚拟机中Java对象的生死判别

    j3_liuliang 通过上面两篇的介绍,相信大家已经知道虚拟机的内存布局和对象创建的过程及在虚拟机中的分布的,那么一个对象创建出来是不可能永生的总会有死亡的时候而虚拟机是如何判定一个对象的生死那就 ...

  2. ART虚拟机 _ Java对象和类的内存结构,java中级面试题库weixin

    当我们想要创建一个java.lang.Class类的实例(类对象)时,以下三种方法可供选择: Class.forName("className") MyClass.class ob ...

  3. java char占用多少字节_Java虚拟机:Java对象大小、对象内存布局及锁状态变化

    一个对象占多少字节? 关于对象的大小,对于C/C++来说,都是有sizeof函数可以直接获取的,但是Java似乎没有这样的方法.不过还好,在JDK1.5之后引入了Instrumentation类,这个 ...

  4. 《深入理解java虚拟机》笔记1——Java内存区域与Java对象

    运行时数据区域 JVM载执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程 ...

  5. Java 并发编程解析 | 如何正确理解Java对象创建过程,我们主要需要注意些什么问题?

    苍穹之边,浩瀚之挚,眰恦之美: 悟心悟性,善始善终,惟善惟道! -- 朝槿<朝槿兮年说> 写在开头 从接触 Java 开发到现在,大家对 Java 最直观的印象是什么呢?是它宣传的 &qu ...

  6. java tcp 编程实例_Java实现基于TCP的通讯程序实例解析

    Java中的TCP通信程序 TCP可以实现两台计算机之间的数据交互通信的两端,要严格区分客户端与服务端 两端通信时的步骤: 1.服务端程序,需要事先启动,等待客户端连接 2.客户端主动连接服务器端,才 ...

  7. HotSpot虚拟机在Java堆中对对象的管理

    在大概了解了Java虚拟机中内存的大致分布后,接下来就应该了解虚拟机是如何在内存中管理对象的,毕竟Java是一门面向对象的语言,在Java程序的运行过程中会不断有对象创建出来.为了方便,这里仅仅以Ho ...

  8. java中用new创建一个对象的过程解析_【漫画】Java对象的创建和访问全过程详解...

    https://github.com/TangBean 漫画由小猿编写创作 仔细看下面的流程图,我们先来获取一个直观的认识,然后再一点一点的进行详细分析! 对象的创建(遇到一条 new 指令时)检查这 ...

  9. Java虚拟机(四)—— Java虚拟机中的对象

    1. Java 对象在虚拟机中的生命周期 在 Java 对象被类加载器加载到虚拟机中后,Java 对象在 Java 虚拟机中有 7 个阶段. 1.1 创建阶段(Created) 创建阶段的具体步骤为: ...

最新文章

  1. C++ exception 类继承结构图
  2. Java static initialization研究
  3. xcode更新之后插件失效的解决办法
  4. node --- 监听文件变化(静态、动态、子进程)
  5. 空值为0非空为1_万达广场4周年,1降到底!0元送万张杂技团门票、人气餐饮6.8折,这波周年庆我先锁为敬...
  6. Hibernate中使用Criteria查询及注解——(DeptTest.java)
  7. idea怎么调试jsp页面_解决idea的c标签错误
  8. Python 模块之科学计算 Pandas
  9. 计算机系统结构sw指令集,自考02325计算机系统结构复习资料六
  10. java协变返回类型_Java中的协变返回类型
  11. 基于css和js的轮播效果图实现
  12. 数电实验三:组合逻辑电路分析与设计
  13. 【软件工程作业3】DFD数据流图和SC结构图
  14. 巃嵸鸿蒙构瑰材兮,明堂赋原文、翻译及赏析_李白古诗_风萧学古网
  15. 直播技术——流媒体协议
  16. Linux系统进程及作业管理
  17. Kudo介绍 + Spark\Python\Scala开发Kudu应用程序
  18. shiro教程1(HelloWorld)
  19. Python语言基础快速入门
  20. win11,google chrome没有声音怎么办

热门文章

  1. 在计算机软件中 BIOS的中文意思是,BIOS是什么意思?电脑主板bios在哪里?
  2. 【云原生 • DevOps】一文掌握容器管理工具 Rancher
  3. 独家!量子通信上市企业2020营收、净利润、研发费用排行榜
  4. 题解 洛谷 P4169 [Violet]天使玩偶/SJY摆棋子【CDQ分治】
  5. 浮点数尾数基值大小(计算机系统结构)
  6. POJ 1700 经典过河问题(贪心)
  7. 准的吓人的“寿命计算器”来了,用五分钟算算自己还能活多久
  8. libgdx 3d_使用Java和libgdx进行3D游戏编程,使用Blender建立模型
  9. java图书分析echarts_【Echarts大数据分析】终于统计出水笔们了!都颤抖把!
  10. skywalking全链路追踪