【JVM】对象的生命周期一 | 对象的创建与存储

Java | 对象的生命周期1-对象的创建与存储

文章目录

  • 前言
  • 对象的创建过程
    • 内存空间的分配方式
      • 方式1 | 指针碰撞
      • 方式2 | 空闲列表
    • 线程安全问题 | 避免空间冲突的方式
      • 方式1 | 同步处理(加锁)
      • 方式2 | 本地线程分配缓存
  • 对象的内存布局
    • Part1 | 对象头
      • Mark Word
      • 类型指针
    • Part2 | 实例数据
    • Part3 | 对齐填充
    • * 示例 | 代码与图
  • 对象的访问定位
    • 方式1 | 句柄
    • 方式2 | 直接指针
    • 差异对比
    • 方法区 与 Java堆 的比较
  • 资料来源
  • 下一篇预览 | 对象的消亡-垃圾回收机制

前言

之前在《Java虚拟机运行时数据分区介绍》一文中介绍过Java对象一般都存储在堆中,本文章将在上篇文章的基础上,详细介绍一下对象的创建过程与堆内存模型。

对象的创建过程

Java程序运行过程中无时无刻不在创建对象。从语言层面上,创建对象通常仅仅是一个new关键字,但是从JVM的角度来说,当遇到一个new指令时,会做以下几件事情。

  1. 检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并且这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有则必须执行类的加载过程。
  2. 给对象分配内存空间,所需内存大小在类加载完成后便可以完全确定(内存空间的分配方式在下一个小结做详细介绍)
  3. JVM还要对对象进行必要的初始化,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等,这些信息存放在对象头之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  4. 在此时所有的字段都为默认的0值,此时要执行构造函数,即Class文件中的<init>()方法。

内存空间的分配方式

一般来说,内存分配有两种方式,分别如下:


方式1 | 指针碰撞

假设所有的对象是在内存中并排排列的,那么我们仅需要一个“分界指针”指出当前最后一个对象所在位置即可(也就是空闲空间的起始位置),这样一来,当创建新的对象的时候,只需要将该指针向后移动一个对象大小的空间即可。

为了方便理解,整张图:

在这里插入图片描述

  1. 当没有新对象进来的时候,分界指针指向空闲空间的起始位置
  2. 当进来新对象之后,假设新对象大小为1kb,那么指针就向后移动1kb的空间,这1kb就用来存储新的对象

该方式适用于标记整理算法、标记复制算法等垃圾回收算法。


方式2 | 空闲列表

如果对象不是有序的存储在内存中的,而是如下图一样散乱的存储(程序的最开始可能是有序的,随着对象被清理而不整理,则可能会出现这种情况),那么JVM会为空闲空间维护一个列表,该列表记录着所有空闲空间的起始位置与大小,当需要new一个新的对象的时候,会在列表中寻找到一个可以放得下该对象的空间,将对象放在该空间中,并更新空闲列表

在这里插入图片描述

  1. 维护空闲空间列表
  2. 新的对象进来,在空闲空间列表中查找可以放得下该对象的空间
  3. 放入对象并更新空闲空间列表

该方式适用于标记清除算法



线程安全问题 | 避免空间冲突的方式

线程安全问题:由于在对象的创建是一件非常频繁的事情,假设有100个线程同时需要创建对象,他们进入到堆内存后(以上述方式1为例),100个线程同时拿到了当前分界指针的位置,并在该位置开始写入内容,那么就会造成内存混乱,100个线程创建的对象互相重写前一个线程写入的内容,最终必定会造成该位置的内容不可用。

在这里插入图片描述

最终的结果可能比我画的图要复杂的多,各个对象的字节码穿插出现。

如何才能避免这种问题呢?JVM采用了两种方案:


方式1 | 同步处理(加锁)

事实上虚拟机采用的是CAS配上失败重试的方式保证更新操作的原子性,当一个线程抢占到锁之后,其它线程只能等待,当该线程执行结束后,该内存空间已经有对象了,且分界指针已经更新,此时其余线程再次抢占锁,如此循环往复,就没有了线程安全问题。


方式2 | 本地线程分配缓存

本地线程分配的方式如下图所示,JVM会为每一个线程分配一个新的空间用来存储接下来该线程中new出来的对象,这样就不会存在空间抢占问题,从而避免了线程安全问题。

在这里插入图片描述

  1. 为每一条线程分配空闲空间
  2. 将线程创建的对象写入其对应的缓存空间中去
  3. 若是缓存空间不足则重新分配空间




对象的内存布局

一个对象由三个部分组成,分别为对象头、实例数据、对齐填充,如下图所示:

在这里插入图片描述

这张图每个内容都有一定的含义,下面会详细介绍。

Part1 | 对象头

对象头一般存储两类信息:


Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、线程持有锁、偏向线程ID、偏向时间戳等。这类数据在长度32位的虚拟机中长度为32比特,在未开启指针压缩技术的64位虚拟机中长度为64比特。官方称此为"Mark Word"。

对象需要存储的运行时数据很多,其实已经超过了64位Bit Map结构所能记录的最大限度,但是对象头中的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽可能多的数据,根据对象的状态复用自己的存储空间。

例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)。


类型指针

类型指针用于指向对象的类元数据。类元数据包含了类的结构信息,如类名、父类、接口、方法、字段等。这部分信息是静态的,不会在运行时发生变化。类型指针的主要作用是使JVM能够快速地找到对象对应的类信息,以便执行方法调用和字段访问等操作。

类型指针通常指向方法区中的类对象。方法区中包含了每个类的类元数据结构(Class Metadata),包括类的常量池、字段和方法的描述符、方法的字节码等。

此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。


Part2 | 实例数据

实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(OrdinaryObject Pointers,OOPs),相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。


Part3 | 对齐填充

对齐填充并不一定是必须存在的,仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍(也就是对象大小必须是8的倍数),对象头已经被精心设计成8的倍数,因此如果对象实例数据不满足8的倍数的话,会用对齐填充来对齐。

* 示例 | 代码与图

我们在Java中定义一个类,内容如下:

public class Example {int num1;byte byte1;boolean bool1;
}

假设对象头占用16字节,字段num1占用4字节,字段byte1占用1字节,字段bool1占用1字节,则对象的布局如下:

在这里插入图片描述

  1. 16字节的对象头

  2. 6字节的实例数据

  3. 由于实例数据不足8位,用2字节的对齐填充进行补齐


对象的访问定位

这一段写的比较多,看起来下划线、加粗、高亮交替出现也比较乱,但是真的很重要也很有趣,我尽量用较少的语言描述清楚。

关于下文会用到的栈、栈帧等内容可以在《【JVM】Java虚拟机运行时数据分区介绍》找到对应介绍。

我们在代码中定位对象的情况无非只有几种(代码如下):

  1. 作为成员变量
  2. 作为成员方法
  3. 作为方法参数
  4. 作为方法中的实例对象

不过,需要注意的是,无论作为哪种方式出现在代码中,最终对他们的操作都一定是在方法中的,而方法是以栈帧的形式出现在栈中的,所以对于对象的任何操作都可以理解成是从栈帧出发寻找到这个对象的存储地址。

OK,深入理解一下上面一段话,会想到什么呢?

有没有什么东西有操作内容却不是方法?

答案就是:代码块与静态代码块

先来简单复习下内容:代码块会在每次创建一个新的对象的时候执行一次,静态代码块会在类加载的过程中(初始化的时候)执行且仅执行一次。

代码块与静态代码块并不会创建栈帧。当类被加载的时候,JVM会为该类生成一个名为<clinit>的方法,静态代码块会加入到该方法中一起执行。当使用构造方法创建一个对象的时候,JVM会为该构造函数创建一个新的栈帧,普通代码块作为构造方法的一部分一起执行,并且在这个栈帧前面运行。

public class JimExample {private JimExample e1; // 实例变量引用,存储在堆中private static JimExample e2; // 类变量引用,存储在方法区的静态区中// 'param' 是一个方法参数引用,存储在栈帧中public void m1(JimExample param) {// 'obj' 是一个局部变量引用,存储在栈帧中JimExample obj = new JimExample();}}

至此我们已经了解到了对于对象的使用(定位寻址)一定是从栈帧中出发的,而对对象的访问主要是通过句柄直接指针来实现的。


方式1 | 句柄

使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference(在Java栈的本地变量表中)存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如下图所示:

在这里插入图片描述

注意:一个是实例数据,一个是类型数据,句柄池中两个指针为一个句柄,两个指针一个指向实例数据,一个指向类型数据。


方式2 | 直接指针

使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图下图所示:

在这里插入图片描述

注意:堆中存储的数据不再是句柄池,而是一个实实在在的对象,只不过该对象有一个指针指向了该对象的对象类型数据上,所以访问完整的对象要先通过reference找到该对象的实例数据,再通过实例数据中的指针找到对象的类型数据。


差异对比

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。


直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发,具体可参见第3章),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。


方法区 与 Java堆 的比较

特性方法区(Method Area)实例池/堆(Instance Pool/Heap)
存储内容类的结构信息、常量池、静态变量、字节码、类的元数据等。通过new关键字创建的对象实例和数组。
生命周期类加载时开始存在,类卸载时移除。对象被创建时开始存在,GC回收时移除。
内存管理使用不同于堆的管理机制,HotSpot JVM中,
JDK 8前为永久代(PermGen),JDK 8后为元空间(Metaspace)。
分为新生代和老年代,新生代包括Eden区、
Survivor区(S0和S1)。
特点存储静态信息,不随程序运行变化,垃圾回收较少。存储动态数据,随程序运行变化,GC频繁执行。



资料来源

周志明:《深入理解Java虚拟机》



下一篇预览 | 对象的消亡-垃圾回收机制

JVM虚拟机每时每刻都在创建对象,但是如果创建出来的对象一直存放在内存中,那么程序的内存早晚有不够用的一天,在这里简单介绍一下C++的对象管理,可能不少资料中都提到过C++的程序员在代码中是神一样的存在,他们掌握着每一个对象的生死,这是由于C++需要使用delete/free关键字(与new关键字对应)手动删除创建出来的对象,代码如下所示:

int* p = new int;  // 动态分配一个整数
*p = 10;          // 使用这个整数
delete p;         // 释放分配的内存

但是Java中并没有这种操作,这是由于JVM有垃圾回收机制,简单来说JVM会定期扫描实例对象是否还“有用”,如果没有用的话就给它清理掉。




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

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

相关文章

内网安全:域内信息探测

1.域内基本信息收集 2.NET命令详解 3.内网主要使用的域收集方法 4.查找域控制器的方法 5.查询域内用户的基本信息 6.定位域管 7.powershell命令和定位敏感信息 1.域内基本信息收集&#xff1a; 四种情况&#xff1a; 1.本地用户&#xff1a;user 2.本地管理员用户&#x…

solidity实战练习1

//SPDX-License-Identifier:MIT pragma solidity ^0.8.24; contract PiggyBank{constructor()payable{emit Deposit(msg.value);//触发事件1//意味着在部署合约的时候&#xff0c;可以向合约发送以太币&#xff08;不是通过调用函数&#xff0c;而是直接在部署合约时发送&#…

外泌体相关基因肝癌临床模型预测——2-3分纯生信文章复现——4.预后相关外泌体基因确定之生存曲线(4)

内容如下: 1.外泌体和肝癌TCGA数据下载 2.数据格式整理 3.差异表达基因筛选 4.预后相关外泌体基因确定 5.拷贝数变异及突变图谱 6.外泌体基因功能注释 7.LASSO回归筛选外泌体预后模型 8.预后模型验证 9.预后模型鲁棒性分析 10.独立预后因素分析及与临床的相关性分析…

[Linux]对Linux中的命令的本质

上回我们讲了Linux的指令&#xff0c;本篇是一个短篇&#xff0c;主要是对命令本质的讲解。 我们知道命令一般都是直接使用的 而可执行程序需要加上当前的路径 &#xff08;这个mytest是我们上上回写的&#xff0c;作用实际是打印Hello world!&#xff09; 我们很直观的可以发…

git为文件添加可执行权限

查看文件权限 git ls-files --stage .\SecretFinder.py100644 表示文件的所有者有读取和写入权限 添加可执行权限 git update-index --chmod x .\SecretFinder.py再次查看文件权限 git ls-files --stage .\SecretFinder.py100755 表示文件的所有者有读取、写入和执行权限

git查看版本,查看安装路径、更新版本

一、查看安装路径 where git查看安装路径 二、更新版本 git update-git-for-windows 更新版本 三、查看版本 git version 查看版本

【鸿蒙学习笔记】文件管理

官方文档&#xff1a;Core File Kit简介 目录标题 文件分类什么是应用沙箱&#xff1f; 文件分类 应用文件&#xff0c;比如应用的安装包&#xff0c;自己的资源文件等。用户文件&#xff0c;比如用户自己的照片&#xff0c;录制的音视频等。 什么是应用沙箱&#xff1f; 应…

maven高级1——一个项目拆成多个

把原来一个项目&#xff0c;拆成多个项目。 &#xff01;&#xff01;他们之间&#xff0c;靠接口通信。 以ssm整合好的项目为例&#xff1a; 如何看拆的ok不ok 只要compile通过就ok。 拆分pojo 先新建一个项目模块&#xff0c;再把内容复制进去。 拆分dao 1.和上面一样…

NI VST 毫米波测试仪器创新

目录 概览​从UHF至V频段的频率覆盖范围&#xff1a;54 GHz远程测量模块​PXIe-5842&#xff1a;VST架构的扩展54 GHz扩频PXIe-5842功能​​宽频覆盖范围​IF和毫米波测试端口可满足多频带需求​高达2 GHz瞬时带宽误差矢量幅度测量性能相位相干同步基于PXI平台集成多种仪器 互补…

maven6——生命周期与插件

生命周期 生命周期&#xff1a;指运行的阶段&#xff08;比如几岁&#xff09; maven有三个生命周期如下&#xff0c;每个生命周期大概做的事情如下&#xff1a; 注意&#xff1a;每次执行某个&#xff0c;他会把上面的都执行一遍 插件&#xff1a; 每一个插件&#xf…

【Git基本操作】创建本地仓库 | 配置本地仓库 | 认识工作区、暂存区、版本库、对象库 | add和commit操作

目录 1.创建Git本地仓库 1.1创建仓库 1.2创建和初始化Git本地仓库 1.3查看隐藏目录.git 2.配置本地仓库 2.1新增配置 2.2删除重置配置 2.3查看配置选项 2.4全局范围的新增和删除配置 3.工作区、暂存区、版本库、对象库 ​4.add操作和commit操作 4.1add操作 4.2com…

labelme 标注检查经验

1. python labelImg.py D:\BaiduNetdiskDownload\yoloDt_qiuyi_num\yoloDt_qiuye_num\train\images D:\BaiduNetdiskDownload\yoloDt_qiuyi_num\yoloDt_qiuye_num\train\labels\classes.txt 2. 目录另存为会找到classes.txt的类&#xff0c;然后标注起来。

idm站点抓取可以用来做什么 idm站点抓取能抓取本地网页吗 idm站点抓取怎么用 网络下载加速器

在下载工具众多且竞争激烈的市场中&#xff0c;Internet Download Manager&#xff08;简称IDM&#xff09;作为一款专业的下载加速软件&#xff0c;仍然能够赢得众多用户的青睐&#xff0c;这都要得益于它的强大的下载功能。我们在开始使用IDM的时候总是有很多疑问&#xff0c…

链接服务器“XX”的OLEDB访问接口“MSOLEDBSQL”返回了消息“登录超时已过期” 解决方法

目录 1. 问题所示2. 原理分析3. 解决方法1. 问题所示 出现如下问题: 与链接服务器的测试连接失败。执行Transact-SQL 语句或批处理时发生了异常。命名管道提供程序:无法打开与SQL SERVER的链接[53]链接服务器“XX”的OLEDB访问接口“MSOLEDBSQL”返回了消息“登录超时已过期…

IntelliJ IDEA中刷新Git分支数据:操作指南与命令详解

前言 在软件开发过程中&#xff0c;频繁地与Git仓库交互是常态&#xff0c;确保本地分支信息与远程仓库保持同步至关重要。IntelliJ IDEA作为一款强大的集成开发环境&#xff0c;提供了直观的图形界面和终端命令行两种方式来帮助开发者高效地管理Git分支。本文将详细介绍如何在…

单词间隔重复算法

间隔重复算法 理论背景 遗忘曲线是一种描述记忆遗忘率的模型&#xff0c;艾宾浩斯在其著作《记忆&#xff1a;实验心理学的贡献》中首次详细描述了遗忘曲线&#xff0c;他使用了一些无意义的字母组合作为记忆对象&#xff0c;通过在不同的时间间隔后检查记忆的遗忘程度&#…

Android列表控件的属性与用法

列表控件的属性与用法 列表控件有Spinner、ListView、RecyclerView、ViewPager等。列表控件的显示一般涉及3个部分&#xff1a;控件、适配器、数据&#xff0c;这三者之间的关系如图1所示。适配器是数据与列表之间的桥梁&#xff0c;适配器中需要将数据中需要显示的属性与列表…

Python(四)---序列

文章目录 前言1.列表1.1.列表简介1.2.列表的创建1.2.1.基本方式[]1.2.2.list()方法1.2.3.range()创建整数列表1.2.4.推导式生成列表 1.3. 列表各种函数的使用1.3.1.增加元素1.3.2.删除元素1.3.3.元素的访问和计数1.3.4.切片1.3.5.列表的排序 1.4.二维列表 2.元组2.1.元组的简介…

智慧营区人员考勤管理系统|DW-S406系统实现无感考勤

智慧营区人员管理系统&#xff08;DW-S406系统&#xff09;通过建设人员基本信息管理功能&#xff0c;实现人力资源可视化、规范化管理&#xff0c;使人力资源管理工作决策的高效化、制度化得到有力保障&#xff0c;真正达到集中管理、集权管理的目标。主要实现营区人员管理、访…

对接高德开放平台API

高德开放平台API&#xff1a; https://lbs.amap.com/ 一、天气查询 天气查询: https://lbs.amap.com/api/webservice/guide/api/weatherinfo adcode城市码表下载: https://lbs.amap.com/api/webservice/download Component public class WeatherUtil {Resourceprivate GdCon…