JVM系列 | 对象的创建与存储

JVM系列 | 对象的生命周期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/798796.html

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

相关文章

SQl server 日期函数查询相关练习

练习1.按月份分析销售数据。 create database date_db; use date_db; CREATE TABLE SalesData ( SaleID INT PRIMARY KEY IDENTITY(1,1), ProductName NVARCHAR(100) NOT NULL, SaleAmount DECIMAL(10, 2) NOT NULL, SaleDate DATE NOT NULL ); INSERT INTO Sa…

mybatis语法进阶1

日志的使用 我们在使用MyBatis的时候, 其实MyBatis框架会打印一些必要的日志信息, 在开发阶段这些日志信息对我们分析问题,理解代码的执行是特别有帮助的; 包括项目上线之后,我们也可以收集项目的错误日志到文件里面去; 所以我们采用专门的日志系统来处理. 步骤 导入坐标拷贝…

算法类学习笔记 ———— 图像金字塔

文章目录 介绍分类高斯金字塔&#xff08;向下采样&#xff09;构建步骤差分高斯金字塔 拉普拉斯金字塔 介绍 图像金字塔是图像中多尺度表达的一种&#xff0c;最主要用于图像的分割&#xff0c;是一种以多分辨率来解释图像的有效但概念简单的结构。图像金字塔最初用于机器视觉…

如何在Mac上恢复已删除的存档文件?

在本文中&#xff0c;我们将分享在 macOS 或 OS X 上运行的 MacBook、iMac 或 Mac mini 上恢复已删除存档文件的不同方法。 下载免费试用的 Mac 数据恢复软件以在 Mac 上恢复已删除的存档文件。 macOS 可以选择压缩您的文件。您只需选择文件&#xff0c;按住 Control 键单击&a…

【排序 - 插入排序 和 希尔排序】

插入排序&#xff08;Insertion Sort&#xff09;是一种简单直观的排序算法&#xff0c;它的工作原理是逐步构建有序序列。在排序过程中&#xff0c;它将未排序的元素逐个插入到已排序的部分中&#xff0c;从而在每次插入时扩展已排序序列的长度。 原理介绍 插入排序的基本思…

【流媒体】 通过ffmpeg硬解码拉流RTSP并播放

简介 目前RTSP拉流是网络摄像头获取图片数据常用的方法&#xff0c;但通过CPU软解码的方式不仅延时高且十分占用资源&#xff0c;本文提供了一种从网络摄像头RTSP硬解码的拉流的方法&#xff0c;并且提供python代码以便从网络摄像头获取图片进行后续算法处理。 下载ffmpeg F…

ArcGIS识别不GDB文件地理数据库显示为空?

​ 点击下方全系列课程学习 点击学习—>ArcGIS全系列实战视频教程——9个单一课程组合系列直播回放 点击学习——>遥感影像综合处理4大遥感软件ArcGISENVIErdaseCognition 我们经常会碰到拷贝的GDB文件ArcGIS无法识别&#xff0c;软件只是把他当做普通的文件夹去看待&am…

主机安全-进程、命令攻击与检测

目录 概述反弹shell原理nc/dev/xxx反弹shell下载不落地反弹Shell各种语言反弹shell linux提权sudosuid提权mysql提权 Dnslog参考 概述 本文更新通过在主机&#xff08;不含容器&#xff09;上直接执行命令或启动进程来攻击的场景。检测方面以字节跳动的开源HIDS elkeid举例。每…

滑块拼图验证码识别

通常滑块验证码都是横向滑动&#xff0c;今天看到一个比较特别的滑块拼图验证码&#xff0c;他不仅能在横向上滑动&#xff0c;还需要进行纵向滑动。如下图所示&#xff1a; 他的滑块在背景图片的左上角&#xff0c;需要鼠标拖动左上角的滑块&#xff0c;移动到背景图的缺口位置…

Apache Doris:下一代实时数据仓库

Apache Doris&#xff1a;下一代实时数据仓库 概念架构设计快速的原因——其性能的架构设计、特性和机制基于成本的优化器面向列的数据库的快速点查询数据摄取数据更新服务可用性和数据可靠性跨集群复制多租户管理便于使用半结构化数据分析据仓一体分层存储 词条诞生技术概述适…

探索性数据分析:使用Python与Pandas库实现数据洞察

探索性数据分析&#xff1a;使用Python与Pandas库实现数据洞察 引言 在当今数据驱动的时代&#xff0c;数据分析已成为决策制定、策略规划和业务优化的关键环节。无论是商业智能、金融分析还是市场研究&#xff0c;数据分析都扮演着至关重要的角色。Pandas库作为Python生态系统…

html02-标签继续学习

1.列表标签 1.1 列表标签的使用场景 场景&#xff1a;在网页中按照 行 展示关联性的内容&#xff0c;如&#xff1a;新闻列表、排行榜、账单等 特点&#xff1a;按照行的方式&#xff0c;整齐显示内容 种类&#xff1a;无序列表、有序列表、自定义列表 1.2无序列表 <!--…

0.单片机工作原理

文章目录 最小系统 单片机芯片 时钟电路 复位电路 电源 最小系统 单片机芯片 本次51单片机的芯片为&#xff1a;STC89C52 Flash(闪存)程序存储器&#xff1a;存储程序的空间 SRAM&#xff1a;数据存储器&#xff0c;可用于存放程序执行的中间结果和过程数据 DPTR&#xff1a;…

中间件——Kafka

两个系统各自都有各自要去做的事&#xff0c;所以只能将消息放到一个中间平台&#xff08;中间件&#xff09; Kafka 分布式流媒体平台 程序发消息&#xff0c;程序接收消息 Producer&#xff1a;Producer即生产者&#xff0c;消息的产生者&#xff0c;是消息的入口。 Brok…

IDEA实现热部署

什么是热部署&#xff1f; 热部署&#xff08;Hot Deployment&#xff09;是指在应用程序运行过程中&#xff0c;无需停止整个应用程序或重新启动服务器&#xff0c;就能够部署新的代码、资源或配置文件&#xff0c;使其立即生效。这种部署方式有助于提高开发效率和系统的可用性…

【数据结构】顺序表的应用

目录 一.引言 二.顺序表概念 三.顺序表的实现 1.定义顺序表 2.顺序表初始化 ​编辑 3.检查空间&#xff0c;如果满了&#xff0c;进行增容 4.顺序表尾插 5.顺序表尾删 6.顺序表头插 7.顺序表头删 ​编辑 8.顺序表查找 9.顺序表在pos位置插入x 10.顺序表删…

【学习笔记】无人机(UAV)在3GPP系统中的增强支持(十四)-无人机操控关键绩效指标(KPI)框架

引言 本文是3GPP TR 22.829 V17.1.0技术报告&#xff0c;专注于无人机&#xff08;UAV&#xff09;在3GPP系统中的增强支持。文章提出了多个无人机应用场景&#xff0c;分析了相应的能力要求&#xff0c;并建议了新的服务级别要求和关键性能指标&#xff08;KPIs&#xff09;。…

sqllabs(第42-53)

第42关 万能密钥登录成功 密码&#xff1a; or 11 -- aaa 修改密码中尝试报错注入 # 获取数据库名 and updatexml(1,concat(0x7e,(select database()),0x7e),1) -- aaa # 获取数据表名 and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_sche…

Unity ColorSpace 之 【颜色空间】相关说明,以及【Linear】颜色校正 【Gamma】的简单整理

Unity ColorSpace 之 【颜色空间】相关说明&#xff0c;以及【Linear】颜色校正 【Gamma】的简单整理 目录 Unity ColorSpace 之 【颜色空间】相关说明&#xff0c;以及【Linear】颜色校正 【Gamma】的简单整理 一、简单介绍 二、在Unity中设置颜色空间 三、Unity中的Gamma…

【STM32开发笔记】搭建VSCode+PyOCD的STM32开发环境

【STM32开发笔记】搭建VSCodePyOCD的STM32开发环境 一、安装软件1.1 安装STM32CubeMX1.2 安装VSCode1.3 安装Arm GNU Toolchain1.4 安装Make for Windows1.5 安装Python1.6 安装PyOCD 二、安装插件2.1 VSCode插件2.2 PyOCD支持包 三、创建项目3.1 创建STM32CubeMX项目3.2 查阅原…