Java对象的揭秘

前言

作为一个 Java 程序员,我们在开发中最多的操作要属创建对象了。那么你了解对象多少?它是如何创建?如何存储布局以及如何使用的?本文将对 Java 对象进行揭秘,以及讲解如何使用 JOL 查看对象内存使用情况。

本文是基于 HotSpot 64 位虚拟机。


一、对象是如何创建的

在这里插入图片描述

1. 类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

关于如何加载详情见 JVM 类加载机制。

2. 分配内存空间

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定(如何确定见下方对象的存储布局)。

为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
根据Java 堆中的内存是否完整,可分为“指针碰撞” 和 “空闲列表” 两种分配方式。

分配方式原理使用场景特点
指针碰撞使用过的内存放到一边,未使用过的内存放在另一边。中间放一个指针作为分界点,然后向空闲区域移动与对象大小相等的距离。堆内存完整简单高效
空闲列表虚拟机维护一个列表,记录上哪些内存块是可用的,在分配的时候,找足够大的内存块儿来划分给对象实例,最后更新列表记录。堆内存不完整 (有空间碎片)较为复杂

那么如何判断 Java 堆中的内存是否完整呢?这个是由采用的垃圾收集器决定:

  • 带有整理内存空间的能力的,如 Serial、Par New 等,内存会比较完整。
  • 基于清除算法的,如 CMS,内存会产生空间碎片。

3. 内存初始化

内存分配完成后,虚拟机必须将分配到的内存空间都初始化为零值(不包括对象头),这步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。

4. 设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息,这些信息存放在对象头中。

另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式(具体描述见下方对象的存储布局)。

5. 执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始。Class 文件中的init方法还没有执行,所有的字段都是默认的零值。

所以一般来说,执行new 指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

二、对象的存储布局

1. 简介

1.1 存储布局

如图,在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:

  • 对象头(header)
  • 实例数据(instancedata)
  • 对齐填充(padding)

在这里插入图片描述

1.2 数据结构

HotSpot 虚拟机使用名为oops(Ordinary Object Pointers) 的数据结构来表示对象。这些oops等同于本地 C 指针。 instanceOops 是一种特殊的oop,表示 Java 中的对象实例。

下图表示对象的数据结构,以及占用内存大小
在这里插入图片描述

1.3 使用 JOL 查看对象内存布局

JOL 是 OpenJDK 官网提供了查看对象内存布局的工具,使用步骤如下。后续的打印的控制台信息都是通过该工具实现的。

  1. 导入依赖
<dependency>
	<groupId>org.codehaus.plexus</groupId>
	<artifactId>plexus-utils</artifactId>
	<version>4.0.0</version>
</dependency>
  1. 使用 JOL 提供的方法
 public static void main(String[] args)  {
	 //查看当前虚拟机信息
	 System.out.println(VM.current().details());

	 //查看对象内部信息
	 System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());

	 //查看对象外部信息包括引用
	 System.out.println(GraphLayout.parseInstance(new Object()).toPrintable());

	 //查看对象占用总大小
	 System.out.println(GraphLayout.parseInstance(new Object()).totalSize());
}

1.4 一个空的 Object 占用多大内存?

空 Object 是只有对象头,没有实例数据,也无需填充对齐。

如图所示,HotSpot 64 位 虚拟机中:

  • 普通对象: 占用 16 字节
  • 数组对象: 占用 24 字节(对象大小须为 8 字节整数倍)

在这里插入图片描述

代码示例:

public static void main(String[] args)  {
	//使用 JOT 查看对象内存
	System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
	System.out.println("---------------------------");
	System.out.println(ClassLayout.parseInstance(new ArrayList<>()).toPrintable());
}

在这里插入图片描述

2. 对象头

在这里插入图片描述

2.1 Mark Word

Mark Word 用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为 4 个字节和 8 个字节。

对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word 被设计成一个有着动态定义的数据结构。

它会根据对象的状态复用自己的存储空间,也就是说在运行期间 MarkWord 里存储的数据会随着锁标志位的变化而变化,如下图为 HotSpot 64 位的对象的存储内容。

在这里插入图片描述

2.2 Klass Pointer

类型指针,即对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。注意是 Klass 不是 Class,Class Pointer是类的指针,而 Klass Pointer指的是底层 c++ 对应的类的指针。

大致流程是一个对象 new 出来以后是被放在堆里的,类的元数据信息是放在方法区里的,在 new 对象的头部有一个指针指向方法区中该类的元数据信息,这个头部的指针就是 Klass Pointer。

并且并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说就是查找对象的元数据信息并不一定要经过对象本身,这点我会在下一节“如何找到对象”具体讨论。

HotSpot 64 位支持指针压缩功能,根据是否开启指针压缩,Class Pointer 占用的大小将会不同:

  • 未开启指针压缩时,占用 8 byte (64bit)
  • 开启指针压缩情况下,占用 4 byte (32bit)

2.3 Length

数组对象特有,表示数组长度,占用 4 字节(32bit)空间,因为虚拟机可以通过普通对象的元数据信息确定对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。

3. 实例数据

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

3.1 基本数据类型

在这里插入图片描述

3.2 引用数据类型

开启指针压缩情况下占 8 字节,开启指针压缩后占 4 字节。

4. 对齐填充

对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是任何对象的大小都必须是 8 字节的整数倍。

对象头部分被精心设计成正好是 8 字节的倍数,因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

如何关闭对齐填充:

# VM参数

# 开启
-XX:+CompactFields
# 关闭
-XX:-CompactFields

如何设置对齐填充长度:

#VM 参数
-XX:ObjectAlignmentInBytes=16

三、对象的指针压缩详解

1. 什么是指针压缩

指针压缩是对类型指针或普通对象指针进行压缩,主要包含以下几种:

压缩指针类型压缩目标压缩变化
压缩类型指针Klass Pointer8 字节变为 4 字节
压缩普通对象指针对象引用8 字节变为 4 字节
数组对象8 字节变为 4 字节

注意:堆内存设置不要超过 32 GB,否则指针压缩会失效。

在 JDK 6 之后的版本中,指针压缩是被默认开启的,可通过启动参数开启或关闭该功能:

# 开启压缩类型指针
-XX:+UseCompressedClassPointers 
# 关闭压缩类型指针
-XX:-UseCompressedClassPointers 

# 开启压缩普通对象指针
-XX:+UseCompressedOops 
# 关闭压缩普通对象指针
-XX:-UseCompressedOops  

代码示例

public static void main(String[] args)  {
	//使用 JOT 查看对象内存
	System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
}

正常默认情况下,指针压缩是开启的,此时类型指针位 4 字节

在这里插入图片描述
在这里插入图片描述

2. JVM 内存不建议超过 32GB?

一般系统内存的最小 IO 单位是字节(byte),按照8 bit 为一组,也就是 1 字节(byte)分配一个地址。指针的 bit 和内存中的 bit 其实是有区别的。指针的 bit 对应着一个内存地址,一个内存地址对应着 1 字节(byte)。

使用 4 字节(32 bit)指针,可以表示 2 32 2^{32} 232个内存地址,而每一个地址指向的是 1 字节(8 bit),故最大可表示 4GB 内存。

通过对象填充我们了解到 Java 对象默认使用了 8 字节对齐填充,也就是我在使用这块内存时候,最小的分配单元就是 8 字节。这样我们的指针指向的地址就是 8 字节,而不是一般系统的 1 字节。

所以虚拟机在开启指针压缩的情况下,Klass Pointer 最大可表示 32GB 内存,超过则指针压缩失效,故不建议堆内存设置超过 32GB

那么如果业务场景内存超过32GB怎么办呢?可以通过修改默认对齐长度进行再次扩展,将对齐长度修改为 16 字节。

3. 指针压缩的原理

我们通过指针压缩,将Klass Pointer 压缩到 4 自己,这样有什么问题吗?原来的指针大小为 8 字节(64 bit),可以表示 2 64 2^{64} 264个内存地址,这样压缩完可以表示的不就少很多?

其实没有。通过上面简介中的描述,了解到 Java 对象默认使用了 8 字节对齐,也就是 1 个对象占用的空间必须是 8 字节的整数倍。

这样就可以通过基址 + 偏移量来表示对象的真正地址,基址其实就是对象地址的开始,但是不一定是 Java 堆的开始地址。

那么这个真正地址怎么计算呢?公式如下,符号不了解可以看运算符这篇文章。

64 位地址 = 基址 + (压缩对象指针 << 对象对齐偏移)

压缩对象指针 = (64 位地址 - 基址) >> 对象对齐偏移

对象对齐偏移与对齐填充相关,它的值就是对齐填充长度的指数值,比如,我们默认的对齐填充长度为 8 字节,也就是 2 3 2^{3} 23,则对象对齐偏移的值就是 3。偏移量就是压缩对象指针 << 3,这就是为什么网上很多文章描述的去掉后三位。

这样虚拟机在定位一个对象时不需要使用真正的内存地址,而是定位偏移量映射后的地址即可。

四、如何找到对象

创建对象自然是为了后续使用该对象,Java 中是通过栈桢里的 reference 数据来指向堆上的具体对象。目前主流的访问方式主要有使用句柄和直接指针两种。

1. 通过句柄

如下图, Java 堆中将可能会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
在这里插入图片描述

2. 通过直接指针

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

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,也是 HotSpot 使用的方式。
在这里插入图片描述


参考:

[1] 周志明. 深入理解 Java 虚拟机(第3版).

[2]峰哥学Java. 对象的内存布局.

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

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

相关文章

云计算如何助力金融科技企业实现高效运营

一、引言 随着信息技术的飞速发展,云计算作为一种新兴的计算模式,正在逐渐改变着传统金融行业的运营模式。金融科技企业作为金融行业的重要组成部分,面临着日益增长的业务需求和技术挑战。在这一背景下,云计算凭借其弹性扩展、高可用性、低成本等优势,成为金融科技企业实…

VisualSVN Server/TortoiseSVN更改端口号

文章目录 概述VisualSVN Server端更改端口号TortoiseSVN客户端更改远程仓库地址 概述 Subversion&#xff08;SVN&#xff09;是常用的版本管理系统之一。部署在服务器上的SVN Server端通常会在端口号80&#xff0c;或者端口号443上提供服务。其中80是HTTP访问方式的默认端口。…

SSM牙科诊所管理系统-计算机毕业设计源码98077

目 录 摘要 1 绪论 1.1研究目的与意义 1.2国内外研究现状 1.3ssm框架介绍 1.4论文结构与章节安排 2 牙科诊所管理系统系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能…

制作ChatPDF之后端Node搭建(三)

后端Node搭建 接上篇:制作ChatPDF之前端Vue搭建&#xff08;二&#xff09; 项目结构 下面是项目的结构图&#xff0c;包括前端 (Vue.js) 和后端 (Node.js) 的项目结构。 pdf-query-app/ ├── frontend/ │ ├── public/ │ │ ├── index.html │ ├── sr…

Python3 match-case 语句

前言 本文主要介绍match-case语句与switch-case的区别&#xff0c;及match-case语句的基本用法。 文章目录 前言一、switch-case 和match-case的区别二、match-case的基本用法1、可匹配的数据类型2、多条件匹配3、通配符匹配 一、switch-case 和match-case的区别 C语言里面s…

C++20实战之channel

C20实战之channel 继前面两节的直播&#xff0c;讲解了thread、jthread、stop_token、stop_source、stop_callback、cv、cv_any等的用法与底层实现&#xff0c;那么如何基于这些知识实现一个小项目呢&#xff1f; 于是引出了这篇&#xff0c;写一个channel出来。 注&#xff1a…

Python-算法编程100例-双指针(入门级)

1、盛水最多的容器 from typing import Listclass Solution:def maxArea(self, height: List[int]) -> int:# 双指针初始化l 0r len(height) - 1max_area 0# 循环终止条件while l < r:# 指针调整条件if height[l] < height[r]:# 指针调整前计算中间值max_area max…

FreeRTOS基础(四):静态创建任务

上一篇博客&#xff0c;我们讲解了FreeRTOS中如何动态创建任务&#xff0c;那么这一讲&#xff0c;我们从实战出发&#xff0c;规范我们在FreeRTOS下的编码风格&#xff0c;掌握静态创建任务的编码风格&#xff0c;达到实战应用&#xff01; 目录 一、空闲任务和空闲任务钩子…

决定短视频打开率的要素:成都鼎茂宏升文化传媒公司

​ 在当下这个短视频盛行的时代&#xff0c;无论是个人创作者还是企业品牌&#xff0c;都希望通过短视频平台获得更多的曝光和关注。然而&#xff0c;如何让自己的短视频在众多内容中脱颖而出&#xff0c;吸引用户的点击和观看&#xff0c;成为了摆在我们面前的重要问题。成都…

【爬虫工具】油管视频批量采集软件

一、背景介绍 1.1 爬取目标 我用Python独立开发了一款爬虫软件&#xff0c;作用是&#xff1a;通过搜索关键词采集ytb的搜索结果&#xff0c;包含14个关键字段&#xff1a;关键词,页码,视频标题,视频id,视频链接,发布时间,视频时长,频道名称,频道id,频道链接,播放数,点赞数,评…

开源模型应用落地-LangSmith试炼-入门初体验-监控和自动化(五)

一、前言 在许多应用程序中&#xff0c;特别是在大型语言模型(LLM)应用程序中&#xff0c;收集用户反馈以了解应用程序在实际场景中的表现是非常重要的。 LangSmith可以轻松地将用户反馈附加到跟踪数据中。通常最好提供一个简单的机制(如赞成和反对按钮)来收集用户对应用程序响…

工控一体机5寸显示器电容触摸屏(YA05WK)产品规格说明书

如果您对工控一体机有任何疑问或需求&#xff0c;或者对如何集成工控一体机到您的业务感兴趣&#xff0c;可移步控芯捷科技。 一、硬件功能介绍 YA05WK是我公司推出的一款新型安卓屏&#xff0c;4核Cortex-A7 架构&#xff0c;主频1.2GHz的CPU。采用12V供电&#xff0c;标配5寸…

使用QT生成二维码的两种方式

目录 使用QRenCode生成二维码编译生成QRenCode库使用QRenCode结果演示优缺点&#xff1a; 使用QZXing进行二维码的编码和解码编译源码使用QZXing库运行结果优缺点 使用QRenCode生成二维码 编译生成QRenCode库 QRenCode开源库 下载好之后使用cmake-gui打开进行构建生成。 点击…

mathtype7.0产品密钥及2024最新软件激活教程步骤

在数字化教育日益普及的今天&#xff0c;如何有效利用技术工具来提高数学学习的效率和质量&#xff0c;成为了教育工作者和学生共同关注的热点。特别是在处理复杂的数学公式、符号以及方程式时&#xff0c;传统的输入方式往往费时费力&#xff0c;且容易出错。为此&#xff0c;…

如何用python做一个用户登录界面——浔川python社

1 需解决的问题&#xff1a; 1.1如何用python做一个用户登录界面&#xff1f; 1.2需要用到哪些库、模块&#xff1f; 2 问题解决&#xff1a; 2.1 回答 1.1 &#xff1a;合理即可&#xff0c;无标准回答。 2.2 回答 1.2 &#xff1a;tk库&#xff08;缩写&#xff09;、GUL界面…

redis基础学习

redis是一个键值对类型的NoSql类型的数据库。 NoSql&#xff08;Non-relational SQL的缩写&#xff0c;也有人看作是not only sql的缩写&#xff09;型数据库&#xff0c;具有以下特征&#xff1a; 1、非结构化&#xff1a;几乎没有约束&#xff0c;约束很少&#xff0c;这要看…

【LLM】两篇多模态LLM综述MultiModal Large Language Models

note &#xff08;一&#xff09;现有的 MM-LLM 的趋势&#xff1a; (1)从专门强调 MM 理解对特定模态的生成的进展&#xff0c;并进一步演变为任何到任何模态的转换&#xff08;例如&#xff0c;MiniGPT-4 → MiniGPT-5 → NExT-GPT&#xff09;&#xff1b; (2) 从 MM PT 提…

神经网络与深度学习——第7章 网络优化与正则化

本文讨论的内容参考自《神经网络与深度学习》https://nndl.github.io/ 第7章 网络优化与正则化 网络优化与正则化 网络优化 网络结构多样性 高维变量的非凸优化 神经网络优化的改善方法 优化算法 小批量梯度下降 批量大小选择 学习率调整 学习率衰减 学习率预热 周期性学习率调…

装甲车启动电源的安全性能分析

装甲车辆启动电源是一种为装甲车辆提供启动动力的专业设备。它通常被用于 火箭兵 、步兵战车、装甲运兵车等JS车辆&#xff0c;这些车辆通常需要较高的启动功率来启动其发动机&#xff0c;尤其是装甲车的发动机&#xff0c;由于其功率大&#xff0c;启动对电力要求很高。在现代…

3DMAX一键虚线图形插件DashedShape使用方法

3DMAX一键虚线图形插件使用方法 3dMax一键虚线图形插件&#xff0c;允许从场景中拾取的样条线创建虚线形状。该工具使你能够创建完全自定义的填充图案&#xff0c;为线段设置不同的材质ID&#xff0c;并在视口中进行方便的预览。 【版本要求】 3dMax 2012 – 2025&#xff08;…