【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会定期扫描实例对象是否还“有用”,如果没有用的话就给它清理掉。




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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

文献翻译与阅读《Integration Approaches for Heterogeneous Big Data: A Survey》

CYBERNETICS AND INFORMATION TECHNOLOGIES’24 论文原文下载地址&#xff1a;原文下载 目录 1 引言 2 大数据概述 3 大数据的异构性 4 讨论整合方法 4.1 大数据仓库&#xff08;BDW&#xff09; 4.2 大数据联盟&#xff08;BDF&#xff09; 5 DW 和 DF 方法的比较、分…

智充科技营收增速放缓:经营成本飙升,应收账款大幅增长

《港湾商业观察》黄懿 6月10日&#xff0c; XCHG Limited 智能充电有限公司(下称&#xff1a;智充科技)在美国证监会(SEC)更新招股书&#xff0c;拟在美国纳斯达克上市&#xff0c;其股票代码为“XCH”。北京智充科技有限公司为其国内运营主体&#xff08;下称“北京智充科技”…

IC后端设计中的shrink系数设置方法

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 在一些成熟的工艺节点通过shrink的方式(光照过程中缩小特征尺寸比例)得到了半节点,比如40nm从45nm shrink得到,28nm从32nm shrink得到,由于半节点的性能更优异,成本又低,漏电等不利因素也可以…

C++学习

一、注释 /*多行 。。。 。。。 注释*/ //单行注释 #include <iostream> using namespace std; int main() {cout << "hellow" << endl;system("pause");return 0; }二、变量定义 #include <iostream> using namespace std; int…

YOLOv8改进 | 注意力机制| 利用并行子网络构建深度较浅但性能卓越的网络【全网独家】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录 &#xff1a;《YOLOv8改进有效…

[PM]原型与交互设计

原型分类 1.草图原型 手绘图稿, 规划的早期,整理思路会使用 2.低保真原型 简单交互, 无需配色, 黑白灰为主, 产品规划和评审阶段使用 标准化的低保真原型是高保真原型的基础 3.高保真原型 复杂交互, 一般用于公开演示, 产品先产出低保真原型, 设计师根据原型产出设计稿 低保…

【ARM】CCI缓存一致性整理

目录 1.CCI500提供的功能 2.CCI500在SOC系统中所处的位置​编辑 3.CCI500内部结构​编辑 4.功能描述 1.CCI500提供的功能 2.CCI500在SOC系统中所处的位置 3.CCI500内部结构 Transaction Tracker&#xff08;TT&#xff09;是用来解决一致性和ordering问题的&#xff0c;它…

【驱动篇】龙芯LS2K0300之spi设备驱动

实验介绍 GC9A01是一款小巧&#xff08;1.28寸&#xff09;、彩色&#xff08;分辨率为 240 * 240 RGB&#xff09;圆形TFT屏幕&#xff0c;它采用4线 SPI的控制方式&#xff0c;电源供电电压为3.3V&#xff0c;有7个控制引脚&#xff1b;本次实验将使用它来验证龙芯SOC的SPI通…

css实现图片渐变切换效果

一、效果 使用csskeyframes&#xff0c;实现5个图片渐变切换的效果。如下图&#xff1a; 二、代码 1.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"w…

头歌资源库(27)特别的数

一、 问题描述 编程输出一个特别的数&#xff0c;该数是一个由1~9组成的9位数&#xff0c;每个数字只能出现一次&#xff0c;且这个9位数由高位到低位前i位能被i整除。 二、算法思想 创建一个长度为9的数组&#xff0c;用于存放1~9这9个数字。使用回溯算法&#xff0c;从第…

(WRF-UCM)高精度城市化气象动力模拟技术

气候变化及应对是政府、科学界及商业界关注的焦点。气候是多个领域&#xff08;生态、水资源、风资源及碳中和等问题&#xff09;的主要驱动因素&#xff0c;合理认知气候变化有利于解释生态环境变化机理及过程&#xff0c;而了解现在、未来气候变化则是进行生态、环境及能源评…

IDEA中配置代理,解决Codearts Snap登陆不了的问题

问题描述&#xff1a;在mac电脑中的idea中安装了华为的codearts snap插件&#xff0c;一直登录不了&#xff0c;账号是没问题的&#xff0c;后来我怀疑是我的代理有问题&#xff0c;找到IDEA中的代理设置先是有这个问题“You have JVM property "https.proxyHost" se…

C++基础(一)

目录 1.不同版本的hello word&#xff01; 2.namespace和&#xff1a;&#xff1a;域作用限定符以及using 2.1 namespace 2.2&#xff1a;&#xff1a; 2.3using用于展开域 3.C输入和输出 4.缺省参数 5.重载 6.引用 6.1引用介绍 6.2 引用的特性 注意&#xff1a; 6.4 c…

C#绘制阻抗圆图初步

阻抗圆图&#xff0c;或者叫史密斯图&#xff0c;是无线电设计方面用的&#xff1b; 基本的阻抗圆图如下&#xff0c; 下面尝试用C#能不能画一下&#xff1b; 先在网上找一个画坐标的C#类&#xff0c;它的效果如下&#xff1b; 自己再增加一个函数&#xff0c;可以绘制中心在…

Redis的安装配置及IDEA中使用

目录 一、安装redis&#xff0c;配置redis.conf 1.安装gcc 2.将redis的压缩包放到指定位置解压 [如下面放在 /opt 目录下] 3.编译安装 4.配置redis.conf文件 5.开机自启 二、解决虚拟机本地可以连接redis但是主机不能连接redis 1.虚拟机网络适配器网络连接设置为桥接模式…

《昇思25天学习打卡营第16天|基于MindNLP+MusicGen生成自己的个性化音乐》

MindNLP 原理 MindNLP 是一个自然语言处理&#xff08;NLP&#xff09;框架&#xff0c;用于处理和分析文本数据。 文本预处理&#xff1a;包括去除噪声、分词、词性标注、命名实体识别等步骤&#xff0c;使文本数据格式化并准备好进行进一步分析。 特征提取&#xff1a;将文…

【嵌入式Linux】<知识点> GDB调试(更新中)

文章目录 前言 一、GDB调试预备工作 二、GDB的启动与退出 三、GDB中查看源代码 四、GDB断点操作 五、GDB调试指令 前言 在专栏【嵌入式Linux】应用开发篇_Linux打工仔的博客中&#xff0c;我们已经写了大量的源程序。但是在调试这些程序时我们都是通过printf大法和肉眼除…

异业联盟整合各大行业门店,共享资源

异业联盟系统是一种将不同行业的企业或商家整合在一起&#xff0c;通过资源共享、优势互补、合作推广等方式&#xff0c;实现共同发展和互利共赢的商业合作模式的数字化管理和运营系统。 其具有以下显著优势&#xff1a; 1.拓展客户群体&#xff1a;不同行业的企业联合起来&am…

Python骨架肌体运动学数学模型

&#x1f3af;要点 &#x1f3af;运动学矢量计算 | &#x1f3af;跳远的运动学计算 | &#x1f3af;关节肢体运动最小加加速度模型 | &#x1f3af;膝关节和踝关节角度二维运动学计算 | &#x1f3af;上下肢体关节连接运动链数学模型 | &#x1f3af;刚体连接点速度加速度计算…

PPTP、L2TP、IPSec、IPS 有什么区别?

随着互联网的发展&#xff0c;保护网络通信的安全越来越重要。PPTP、L2TP、IPSec、IPS是常见的网络安全协议和技术&#xff0c;在保护网络通信安全方面发挥着不同的作用和特点。下面介绍PPTP、L2TP、IPSec、IPS之间的区别。 点对点隧道协议&#xff08;PPTP&#xff09;是一种用…