Java对象内存结构和创建过程

news2025/8/7 6:00:23

文章目录

  • 对象的内存布局
    • 对象头
      • Mark Word
      • Klass Pointer
    • 实例数据
    • 对齐数据
  • 对象的创建
      • 总结

对象的内存布局

我们的对象一般存储在我们的堆内存中,我们把实例对象可以划分为对象头,实例数据,对齐填充

  • 对象头(object header):有两类信息
    1. 存储对象自身运行时的数据,如哈希码,GC分代年龄,锁状态,线程持有的锁,偏向线程ID等,将这些数据存储在Mark Word
    2. 另一部分存储的是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例
  • 实例数据(Instance Data):存储的是对象真正有效的信息
  • 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。

img

对象头

我们可以在Hotspot官方文档中找到它的描述(下图)。从中可以发现,它是Java对象和虚拟机内部对象都有的共同格式,由两个(计算机术语)组成。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

img

它里面提到了对象头由两个组成,这两个是什么呢?我们还是在上面的那个Hotspot官方文档中往上看,可以发现还有另外两个名词的定义解释,分别是 mark wordklass pointer

Mark Word

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。

Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit,因为对象头中要存储的数据已经超过了64bit的限制,考虑到了了虚拟机的空间效率,所以Mark Word被设计成动态定义的数据结构,以便在极小的内存空间存储尽量多的数据,根据对象的状态来复用我们的存储空间

在32位JVM中是这么存储的

img

在64位JVM中是这么存的

img

  • 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
    • 01 表示无锁或者偏向锁
    • 00表示轻量级锁
    • 10表示重量级锁
    • 11表示对象待回收的状态
  • biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  • 分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。 上锁的对象是不能进行回收的
  • 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁状态下,hashcode会被转移到Monitor中
  • 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
  • epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。

Klass Pointer

即类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。JVM使用的是直接指针

对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于在Java虚拟机规范里面只规定了 reference类型 是一个指向对象的引用,并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置,对象访问方式也是取决于虚拟机实现而定的主流的访问方式有使用句柄和直接指针两种。
如果使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。如图1所示。

img
图1 通过句柄访问对象

如果使用直接指针访问的话,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如图2所示。

img
图2 通过直接指针访问对象

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

实例数据

如果对象有属性字段,则这里会有数据信息。如果对象无属性字段,则这里就不会有数据。根据字段类型的不同占不同的字节,例如boolean类型占1个字节,int类型占4个字节,oops(引用类型)等等;

对齐数据

对象可以有对齐数据也可以没有。默认情况下,Java虚拟机堆中对象的起始地址需要对齐至8的倍数。如果一个对象用不到8N个字节则需要对其填充,以此来补齐对象头和实例数据占用内存之后剩余的空间大小。如果对象头和实例数据已经占满了JVM所分配的内存空间,那么就不用再进行对齐填充了。

所有的对象分配的字节总SIZE需要是8的倍数,如果前面的对象头和实例数据占用的总SIZE不满足要求,则通过对齐数据来填满。

为什么要对齐数据?字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。

对象的创建

这里我们只考虑new关键字(复制,反序列化,反射不考虑)和普通对象(不包括class对象和数组对象)

1)检查是否有对象对应的类信息

当我们的Java虚拟机碰到了一条字节码new指令时

  • 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用是否被加载,解析,初始化过,没有则会执行相应的类加载过程

2)分配内存

对象所需的内存的大小在类加载完成后便可完全确定,对应堆内存的是否是绝对规整的,采用不同的方式分配内存

  • 对于规整的堆内存,采用指针碰撞
  • 对于不规则的堆内存,需要一个空闲列表的数据结构来实现内存的分配

关于内存分配的安全性

我们知道创建对象在JVM中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发的情况下也并不是线程安全的,比如出现在给A分配内存,指针还没有来得及修改,对象又同时使用了原来的指针进行分配内存

  • 使用CAS来配上失败重试的方式来保证更新操作的原子性
  • 另一种就是把内存分配的动作按照线程划分到不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程缓冲区(TLAB),每个线程要分配内存,就在自己的本地缓冲区分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定

3 )对对象进行必要的设置

  • 必须将分配到的内存空间(不包括对象头)都初始化位零值,这步操作保证了对象的实例字段在Java代码可以不赋值就可以直接使用
  • 对对象进行一些必要的设置,例如这个对象是哪个类的实例(如何找到类的元数据),对象的哈希码(实际上是延后到真正调用 hashcode才会计算),对象的GC分代年龄等

4 )构造函数的执行

  • 在上面的一系列操作,在JVM来看,一个对象已经产生了,但是在Java程序来说,只是刚刚开始,我们需要来执行我们的对应的构造方法来执行对象的初始化,这样一个真正的对象才算完全被构造出来

总结

简单类对象的实例化过程

1、在方法区加载类;

2、在栈内存申请空间,声明变量P;

3、在堆内存中开辟空间,分配对象地址;

4、在对象空间中,对对象的属性进行默认初始化,类成员变量显示初始化;

5、构造方法进栈,进行初始化;

6、初始化完成后,将堆内存中的地址赋给引用变量,构造方法出栈;

子类对象的实例化过程

1、在方法区先加载父类,再加载子类;

2、在栈中申请空间,声明变量P;

3、在堆内存中开辟空间,分配对象地址;

4、在对象空间中,对对象的属性(包括父类的属性)进行默认初始化;

5、子类构造方法进栈;

6、显示初始化父类的属性;

7、父类构造方法进栈,执行完毕出栈;

8、显示初始化子类的属性;

9、初始化完毕后,将堆内存中的地址值赋给引用变量P,子类构造方法出栈;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/15690.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot+Vue项目流浪狗领养管理系统的设计与实现

文末获取源码 开发语言:Java 使用框架:spring boot 前端技术:JavaScript、Vue 、css3 开发工具:IDEA/MyEclipse/Eclipse、Visual Studio Code 数据库:MySQL 5.7/8.0 数据库管理工具:phpstudy/Navicat JDK版…

图像运算和图像增强十

图像运算和图像增强十 图像锐化之 Sobel、Laplacian 算子实现边缘检测 (1)Sobel算子(一阶微分算子) Sobel算子是一种用于边缘检测的离散微分算子,它结合了高斯平滑和微分求导。该算子用于计算图像明暗程度近似值,根据图像边缘旁…

top命令应用(查看进程实时动态信息)

记录:321 场景:在CentOS 7.9操作系统上,top命令是查看进程实时动态信息工具。查看进程状态、进程使用内存状况、进程使用CPU状况、进程PID等。 版本: 操作系统:CentOS 7.9 1.top命令介绍 top命令,查看…

内存、指针与数组

C语言的指针可以当成一个特殊的数据类型(像int一样的数据类型),可以说其唯一的作用就是为了存储地址,其他的都可以当作它的衍生用法。 指针的诸多功能都是基于其能直接操作指定内存空间存储的值,每个程序运行都会由操作…

git新建仓库提交项目代码+常用命令

一:新建仓库 输入一下仓库名称,归属和路径都是生成的不需要自己去编辑 点击创建就创建了一个新的仓库,下面就是仓库刚创建好的样子 二:向仓库里提交项目代码 首先打开你要提交的项目文件: 根据官方的提示去提交代码&…

Linux:shell编程2(内含:1.设置环境变量+2.位置参数变量+3.预定义变量+运算符+4.条件判断)

写在开头: 小技巧:除了赋值不加空格,其他的,例如是[ ] ()等都需要空格! 1.设置环境变量: 注:类似于C语言全局变量 案例1:在/etc/profile文件中定义TOMCAT_HOME环境变量。 解释&…

洛谷 模拟 普及-

文章目录💥前言😉解题报告💥一、快乐水🤔一、题意及思路:😎二、源码:😮三、代码分析:💥二、漂亮的绝杀🤔一、题意及思路:😎二、源码:&…

小学生python游戏编程arcade----坦克大战2

小学生python游戏编程arcade----坦克大战2前言多摄象头显得分,title地图加载,精灵分层管理,移动精灵1、提示框制作1.1养眼绿色1.2 画距形提示框1.3 效果图1.4 提示框加提示2、子弹计数问题2.1 初始时给一定的子弹量2.2 发射子弹时进行控制2.3…

hevc 半像素

1 分数像素精度运动估计 物体在连续帧间的运动是连续的,而像素本身是离散的,这种现象带来了一个问题,当前帧中图像块的最佳参考块不一定位于参考帧的证书像素点位置,为了更加精确的预测当前带编码的图像块,有必要在非整…

海运整柜出口操作流程有哪些注意事项?

货物运输时,海运是一种非常常见的形式,根据货物的不同,海运也有很多形式的货物装运,海运整柜就是其中之一。 海运整柜大致分为20GP/40/GP/40HQ。是指只有一个发货人将整箱货物运到目的港,比较容易竞争。发货人负责装箱…

IntentService 源码理解

一、概述 本篇文章讲解的是分析IntentService源码并使用,安卓API迭代更新的太快,IntentService已经在Android8.0 (API 26)之后就不推荐使用了,在Android API 30正式弃用,官方建议用JobIntentService 或 WorkManager替代&#xff0…

为什么要少用全局变量

为什么要少用全局变量?甚至有些公司禁止用全局变量。有一个说法是这样的,全局变量的最佳前缀是什么?答:// 接下来就粗略说说这个问题。 1、全局变量和局部变量 (1)全局变量:定义在函数外&…

RocketMQ NameServer 概览

🍊 Java学习:Java从入门到精通总结 🍊 深入浅出RocketMQ设计思想:深入浅出RocketMQ设计思想 🍊 绝对不一样的职场干货:大厂最佳实践经验指南 📆 最近更新:2022年11月18日 &#…

析构函数详解

析构函数1.概念与特性2.工作原理4.析构的顺序如果一个类中什么成员都没有,那么该类简称为空类。而空类中其实并不是真的什么都没有,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。构造函数:主要完成初始化工作析构函…

内网渗透神器CobaltStrike之配置与基础操作(一)

CobaltStrike简介 Cobalt Strike: C/S架构的商业渗透软件,适合多人进行团队协作,可模拟APT做模拟对抗,进行内网渗透。 Cobalt Strike 一款GUI的框架式渗透工具,集成了端口转发、服务扫描,自动化溢出,多模…

megahit源码迁移解析

megahit源码迁移大作业 在进行megahit源码迁移前需要清理自己的实验环境 1、链接鲲鹏服务器 2、进入源码存放地址/opt/portadv/portadmin/sourcecode 环境准备,清理之前实验环境后下载本次实验所需的源码 之前存在的其他文件,删除code 登录代码迁移工…

Python常见操作的时间复杂度

Python常见操作的时间复杂度 本文整理了Python中常见数据结构操作的时间复杂度,旨在帮助大家了解Python操作的性能,协助运行更快的代码。 文章目录标注方法List操作Set操作Deque操作标注方法 程序时间复杂度一般用"大O表示法(Big-O no…

windows11系统WSL2安装ubuntu20.04桌面

文章目录1. MobaXterm安装2.WSL安装xfce desktop3. 连接桌面参考链接1. MobaXterm安装 这个比较简单,没介绍 2.WSL安装xfce desktop 安装命令 sudo apt-get install xfce4-terminal sudo apt-get install xfce4安装完之后需要稍微配置一下: export …

【并发编程六】c++进程通信——信号量(semaphore)

【并发编程六】c进程通信——信号量(semaphore)一、概述二、信号量三、原理四、过程1、进程A过程2、进程B过程五、demo1、进程A2、进程B六、输出七、windows api介绍1. 创建信号量 CreateSemaphore()2. 打开信号量 OpenSemaphore()3. 等待 WaitForSingle…

力扣LeatCode算法题第三题-无重复字符的最长子串

要求: 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 我一开始采用的第一种方法是使用hashmap去比对大小,在idea上可以跑通程序,但在leatcode的编译器中,无法通过字符串s"" 和s"…