一看就懂的java对象内存布局

news2024/11/27 0:15:29

前言

Java 中一切皆对象,同时对象也是 Java 编程中接触最多的概念,深入理解 Java 对象能够更帮助我们深入地掌握 Java 技术栈。在这篇文章里,我们将从内存的视角,带你深入理解 Java 对象在虚拟机中的表现形式。

学习路线图:

1. 对象在哪里分配?

在 Java 虚拟机中,Java 堆和方法区是分配对象的主要区域,但是也存在一些特殊情况,例如 TLAB、栈上分配、标量替换等。 这些特殊情况的存在是虚拟机为了进一步优化对象分配和回收的效率而采用的特殊策略,可以作为知识储备。

  • 1、Java 堆(Heap): Java 堆是绝大多数对象的分配区域,现代虚拟机会采用分代收集策略,因此 Java 堆又分为新生代、老生代和永生代。如果新生代使用复制算法,又可以分为 Eden 区、From Survivor 区和 To Survivor 区。除了这些每个线程都可以分配对象的区域,如果虚拟机开启了 TLAB 策略,那么虚拟机会在堆中为每个线程预先分配一小块内存,称为线程本地分配缓冲(Thread Local Allocation Buffer,TLAB)。在 TLAB 上分配对象不需要同步锁定,可以加快对象分配速度(TLAB 中的对象依然是线程共享读取的,只是不允许其他线程在该区域分配对象);

  • 2、方法区(Method Area): 方法区也是线程共享的区域,堆中存放的是生命周期较短的对象,而方法区中存放的是生命周期较长的对象,通常是一些支撑虚拟机执行的必要对象,将两种对象分开存储体现的是动静分离的思想,有利于内存管理。存储在方法区中的数据包括已加载的 Class 对象、静态字段(本质上是 Class 对象中的实例字段,下文会解释)、常量池(例如 String.intern())和即时编译代码等;

  • 3、栈上分配(Stack Allocation): 如果 Java 虚拟机通过逃逸分析后判断一个对象的生命周期不会逃逸到方法外,那么可以选择直接在栈上分配对象,而不是在堆上分配。栈上分配的对象会随着栈帧出栈而销毁,不需要经过垃圾收集,能够缓解垃圾收集器的压力。

  • 4、标量替换(Scalar Replacement): 在栈上分配策略的基础上,虚拟机还可以选择将对象分解为多个局部变量再进行栈上分配,连对象都不创建。

2. 对象的访问定位

Java 类型分为基础数据类型(int 等)和引用类型(Reference),虽然两者都是数值,但却有本质的区别:基础数据类型本身就代表数据,而引用本身只是一个地址,并不代表对象数据。那么,虚拟机是如何通过引用定位到实际的对象数据呢?具体访问定位方式取决于虚拟机实现,目前有 2 种主流方式:

  • 1、直接指针访问: 引用内部持有一个指向对象数据的直接指针,通过该指针就可以直接访问到对象数据。采用这种方式的话,就需要在对象数据中额外使用一个指针来指向对象类型数据;

  • 2、句柄访问: 引用内部持有一个句柄,而句柄内部持有指向对象数据和类型数据的指针(句柄位于 Java 堆中句柄池)。使用这种方式的话,就不需要在对象数据中记录对象类型数据的指针。

使用句柄的优点是当对象在垃圾收集过程中移动存储区域时,虚拟机只需要改变句柄中的指针,而引用保持稳定。而使用直接指针的优点是只需要一次指针跳转就可以访问对象数据,访问速度相对更快。以 Sun HotSpot 虚拟机而言,采用的是直接指针方式,而 Android ART 虚拟机采用的是句柄方式。

handle.h

// Android ART 虚拟机源码体现:// Handles are memory locations that contain GC roots. As the mirror::Object*s within a handle are// GC visible then the GC may move the references within them, something that couldn't be done with// a wrap pointer. Handles are generally allocated within HandleScopes. Handle is a super-class// of MutableHandle and doesn't support assignment operations.template<class T>class Handle : public ValueObject {	...}

直接指针访问:

句柄访问:

关于 Java 引用类型的深入分析,见 引用类型

3. 使用 JOL 分析对象内存布局

这一节我们演示使用 JOL(Java Object Layout) 来分析 Java 对象的内存布局。JOL 是 OpenJDK 提供的对象内存布局分析工具,不过它只支持 HotSpot / OpenJDK 虚拟机,在其他虚拟机上使用会报错:

错误日志

java.lang.IllegalStateException: Only HotSpot/OpenJDK VMs are supported

3.1 使用步骤

现在,我们使用 JOL 分析 new Object() 在 HotSpot 虚拟机上的内存布局,模板程序如下:

示例程序

// 步骤一:添加依赖implementation 'org.openjdk.jol:jol-core:0.11'
// 步骤二:创建对象Object obj = new Object();
// 步骤三:打印对象内存布局
// 1. 输出虚拟机与对象内存布局相关的信息System.out.println(VM.current().details());
// 2. 输出对象内存布局信息

System.out.println(ClassLayout.parseInstance(obj).toPrintable());

输出日志

# Running 64-bit HotSpot VM.# Using compressed oop with 3-bit shift.# Using compressed klass with 3-bit shift.# Objects are 8 bytes aligned.# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]java.lang.Object object internals: OFFSET  SIZE   TYPE DESCRIPTION        VALUE      0     4        (object header)    01 00 00 00 (00000001 00000000 00000000 00000000) (1)      4     4        (object header)    00 00 00 00 (00000000 00000000 00000000 00000000) (0)      8     4        (object header)    e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)     12     4        (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

其中关于虚拟机的信息:

  • Running 64-bit HotSpot VM. 表示运行在 64 位的 HotSpot 虚拟机;

  • Using compressed oop with 3-bit shift. 指针压缩(后文解释);

  • Using compressed klass with 3-bit shift. 指针压缩(后文解释);

  • Objects are 8 bytes aligned. 表示对象按 8 字节对齐(后文解释);

  • Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] :依次表示引用、boolean、byte、char、short、int、float、long、double 类型占用的长度;

  • Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] :依次表示数组元素长度。

我将 Java 对象的内存布局总结为以下基本模型:

3.2 对象内存布局的基本模型

在 Java 虚拟机中,对象的内存布局主要由 3 部分组成:

  • 1、对象头(Header): 包括对象的运行时状态信息 Mark Work 和类型指针(直接指针访问方式),数据对象还会记录数组元素个数;

  • 2、实例数据(Instance Data): 普通对象的实例数据包括当前类声明的实例字段以及父类声明的实例字段,而 Class 对象的实例数据包括当前类声明的静态字段和方法表等;

  • 3、对齐填充(Padding): HotSpot 虚拟机对象的大小必须按 8 字节对齐,如果对象实际占用空间不足 8 字节的倍数,则会在对象末尾增加对齐填充。

关于方法表的作用,见 重载与重写。

4. 对象内存布局详解

这一节开始,我们详细解释对象内存布局的模型。

4.1 对象头(Header)**

  • Mark Work: Mark Work 是对象的运行时状态信息,包括哈希码、分代年龄、锁状态、偏向锁信息等。由于 Mark Work 是与对象实例数据无关的额外存储成本,因此虚拟机选择将其设计为带状态的数据结构,会根据对象当前的不同状态而定义不同的含义;

  • 类型指针(Class Pointer): 指向对象类型数据的指针,只有虚拟机采用直接指针的对象访问定位方式才需要在对象上记录类型指针,而采用句柄的对象访问定位方式不需要此指针;

  • 数组长度: 数组类型的元素长度是不能提前确定的,但在创建对象后又是固定的,所以数组对象的对象头中会记录数组对象中实际元素的个数。

以下演示查看数组对象的对象头中的数组长度字段:

示例程序

char [] str = new char[2];System.out.println(ClassLayout.parseInstance(str).toPrintable());

输出日志

[C object internals: OFFSET  SIZE   TYPE DESCRIPTION        VALUE      0     4        (object header)    01 00 00 00 (00000001 00000000 00000000 00000000) (1)      4     4        (object header)    00 00 00 00 (00000000 00000000 00000000 00000000) (0)      8     4        (object header)    41 00 00 f8 (01000001 00000000 00000000 11111000) (-134217663)     12     4        (object header)    【数组长度:2】02 00 00 00 (00000010 00000000 00000000 00000000) (2)     16     4   char [C.<elements>     N/A     20     4        (loss due to the next object alignment)Instance size: 24 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,对象头中有一块 4 字节的区域,显示该数组长度为 2。

4.2 实例数据(Instance Data)

普通对象和 Class 对象的实例数据区域是不同的,需要分开讨论:

  • 1、普通对象: 包括当前类声明的实例字段以及父类声明的实例字段,不包括类的静态字段;

  • 2、Class 对象: 包括当前类声明的静态字段和方法表等

其中,父类声明的实例字段会放在子类实例字段之前,而字段间的并不是按照源码中的声明顺序排列的,而是相同宽度的字段会分配在一起:引用类型 > long/double > int/float > short/char > byte/boolean。如果虚拟机开启 CompactFields 策略,那么子类较窄的字段有可能插入到父类变量的空隙中。

4.3 对齐填充(Padding)

HotSpot 虚拟机对象的大小必须按 8 字节对齐,如果对象实际占用空间不足 8 字节的倍数,则会在对象末尾增加对齐填充。 对齐填充不仅能够保证对象的起始位置是规整的,同时也是实现指针压缩的一个前提。

5. 什么是指针压缩?

我们都知道 CPU 有 32 位和 64 位的区别,这里的位数决定了 CPU 在内存中的寻址能力,32 位的指针可以表示 4G 的内存空间,而 64 位的指针可以表示一个非常大的天文数字。但是,目前市场上计算机的内存中不可能有这么大的空间,因此 64 位指针中很多高位比特其实是被浪费掉的。 为了提高内存利用效率,Java 虚拟机会采用指针压缩的方式,让 32 位指针不仅可以表示 4G 内存空间,还可以表示略大于 4G (不超过 32 G)的内存空间。这样就可以在使用较大堆内存的情况下继续使用 32 位的指针变量,从而减少程序内存占用。 但是,32 位指针怎么可能表示超过 4G 内存空间?我们把 64 位指针的高 32 位截断之后,剩下的 32 位指针也最多只能表示 4G 空间呀?

在解释这个问题之前,我先解释下为什么 32 位指针可以表示 4G 内存空间呢? 细心的同学会发现,你用 $2^{32}$ 计算也只是得到 512M 而已,那么 4G 是怎么计算出来的呢?其实啊,操作系统中最小的内存分配单位是字节,而不是比特位,操作系统无法按位访问内存,只能按字节访问内存。因此,32 位指针其实是表示 $2^{32}bytes$ ,而不是 $2^{32}bits$,算起来就是 4G 内存空间。

理解了 4G 的计算问题后,再解释 32 位指针如何表示 32G 内存空间就很简单了。 这就拐回到上一节提到的对象 8 字节对齐了。操作系统将 8 个比特位组合成 1 个字节,等于说只需要标记每 8 个位的编号,而 Java 虚拟机在保证对象按 8 字节对齐后,也可以只需要标记每 8 个字节的编号,而不需要标记每个字节的编号。因此,32 位指针其实是表示 $2^{32}*8bytes$,算起来就是 32G 内存空间了。如下图所示:

提示: 在上文使用 JOL 分析对象内存布局时,输入日志 Using compressed oop with 3-bit shift. 就表示对象是按 8 字节对齐,指针按 3 位位移。

那对象对齐填充继续放大的话,32 位指针是不是可以表示更大的内存空间了?对。 同理,对齐填充放大到 16 位对齐,则可以表示 64G 空间,放大到 32 位对齐,则可以表示 128G 空间。但是,放大对齐填充等于放大了每个对象的平大小,对齐越大填充的空间会越快抵消指针压缩所减少的空间,得不偿失。因此,Java 虚拟机的选择是在内存空间超过 32G 时,放弃指针压缩策略,而不是一味增大对齐填充。

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

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

相关文章

2023第二届全国大学生数据分析大赛A题思路

某电商平台用户行为分析与挖掘 背景&#xff1a;电商是当今用户最大的交易市场之一&#xff0c;电商行业也逐渐成熟&#xff0c; 所有市场中可售卖的商品全都在平台中存在&#xff0c;并且在网络和疫情的影 响下&#xff0c;在线上的消费行为满足全年龄段用户。 用户的交易行为…

unittest 通过TextTestRunner(buffer=True)打印断言失败case的输出内容

buffer是unittest.TextTestRunner的一个参数&#xff0c;它决定了测试运行时是否将输出结果缓存&#xff0c;并在测试完成后一次性打印。 当buffer设置为True时&#xff0c;测试运行期间的输出结果会被缓存起来&#xff0c;并在测试完成后一次性打印。这对于一些输出频繁的测试…

Lamport Clock算法

Lamport Clock 是一种表达逻辑时间的逻辑时钟&#xff08;logical clock&#xff09;&#xff0c;能够计算得到历史事件的时间偏序关系。 假设 P0进程是分布式集群中心节点中的监控者&#xff0c;用于统一管理分布式系统中事件的顺序。其他进程在发送消息之前和接受事件消息之后…

操作系统——内存映射文件(王道视频p57)

1.总体概述&#xff1a; 2.传统文件访问方式&#xff1a; 我认为&#xff0c;这种方式最大的劣势在于&#xff0c;如果要对整个文件的不同部分进行多次操作的话&#xff0c;这样确实开销可能会大一些&#xff0c;而且程序员还要指定对应的“分块”载入到内存中 3.内存映射文件…

Qt的事件

2023年11月5日&#xff0c;周日上午 还没写完&#xff0c;不定期更新 目录 事件处理函数的字体特点Qt事件处理的工作原理一些常用的事件处理函数Qt中的事件类型QEvent类的type成员函数可以用来判断事件的类型事件的类型有哪些&#xff1f;有多少种事件类 事件处理函数的字体特…

unittest 通过TextTestRunner(failfast=True),失败或错误时停止执行case

failfast是unittest.TextTestRunner的一个参数&#xff0c;它用于控制测试运行过程中遇到第一个失败或错误的测试方法后是否立即停止执行。 当failfast设置为True时&#xff0c;一旦发现第一个失败或错误的测试方法&#xff0c;测试运行就会立即停止&#xff0c;并输出相应的失…

插值表达式 {{}}

前言 持续学习总结输出中&#xff0c;今天分享的是插值表达式 {{}} Vue插值表达式是一种Vue的模板语法&#xff0c;我们可以在模板中动态地用插值表达式渲染出Vue提供的数据绑定到视图中。插值表达式使用双大括号{{ }}将表达式包裹起来。 1.作用&#xff1a; 利用表达式进行…

教你烧录Jetson Orin Nano的ubuntu20.04镜像

Jetson Orin Nano烧录镜像 视频讲解 教你烧录Jetson Orin Nano的ubuntu20.04镜像 1. 下载sdk manager https://developer.nvidia.com/sdk-manager sudo dpkg -i xxxx.deb2. 进入recovery 插上typeC后&#xff0c;短接J14的FORCE_RECOVERY和GND&#xff0c;上电 如下图&#…

【调度算法】单机调度遗传算法

问题描述 工件ABCDEFG工件编号0123456加工时间4765835到达时间3245321交货期10153024141320 目标函数 最小化交货期总延时时间 运算结果 最佳调度顺序&#xff1a; [6, 3, 2, 5, 0, 1, 4] 最小交货期延时时间&#xff1a; 47python代码 import random import numpy as np…

自动驾驶行业观察之2023上海车展-----智驾供应链(3)

智驾解决方案商发展 华为&#xff1a;五项重磅技术更新&#xff0c;重点发布华为ADS 2.0和鸿蒙OS 3.0 1&#xff09;产品方案&#xff1a;五大解决方案都有了全面的升级&#xff0c;分别推出了ADS 2.0、鸿蒙OS 3.0、iDVP智能汽车数字平台、智能车云服务和华为车载光最新 产品…

linux下使用vscode对C++项目进行编译

项目的目录结构 头文件swap.h 在自定义的头文件中写函数的声明。 // 函数的声明 void swap(int a,int b);swap.cpp 导入函数的声明&#xff0c;写函数的定义 #include "swap.h" // 双引号表示自定义的头文件 #include <iostream> using namespace std;// 函…

2023年中国商业密码行业研究报告

第一章 行业概况 1.1 定义及分类 根据《密码法》相关规定&#xff0c;密码是指采用特定变换的方法对信息等进行加密保护、安全认证的技术、产品和服务。 密码产业是指为了保障信息安全&#xff0c;提供加密保护、安全认证相关技术、产品和服务的相关行业总称&#xff0c;主 要…

为机器学习算法准备数据(Machine Learning 研习之八)

本文还是同样建立在前两篇的基础之上的&#xff01; 属性组合实验 希望前面的部分能让您了解探索数据并获得洞察力的几种方法。您发现了一些数据怪癖&#xff0c;您可能希望在将数据提供给机器学习算法之前对其进行清理&#xff0c;并且发现了属性之间有趣的相关性&#xff0c…

python 机器学习 常用函数

一 np.random.randint "randint" 是 "random integer" 的缩写&#xff0c;表示生成随机整数。 np.random.randint 是 NumPy 库中的一个函数&#xff0c;用于生成随机整数。以下是该函数的一般语法&#xff1a; np.random.randint(low, high, size)其中…

MongDB 的安装 无废话

MongDB 的安装 1 安装 MongDB https://www.mongodb.com/try/download/community-kubernetes-operator 这里我们选择 ZIP 解压到文件夹 创建 data 文件 在 data 文件夹里面创建 db 和 logs 文件夹 进入 bin 目录 输入 cmd 回车 2 启动 MongDB 输入启动命令 mongod --dbpath..\…

[JavaWeb]——Spring事务管理和@Transactional注解

&#x1f308;键盘敲烂&#xff0c;年薪30万&#x1f308; 目录 Spring中的事务管理 问题抛出&#xff1a; 解决方案&#xff1a; Transactional注解&#xff1a; rollbackFor属性&#xff1a; propagation属性&#xff1a; 应用&#xff1a; &#x1f4d5;总结 知识回…

kubesphere部署尚医通

目录​​​​​​​ 项目架构 中间件 deploy.yaml 修改maven从阿里云下载镜像 部署到k8s集群 项目架构 yygh-parent |---common //通用模块 |---hospital-manage //医院后台 [9999] |---model …

机器学习框架TensorFlow.NET环境搭建1(C#)

测试环境 visual studio 2017 window10 64位 测试步骤如下&#xff1a; 1 新建.net framework控制台项目&#xff0c;工程名称为TensorFlowNetDemo&#xff0c;.net framework的版本选4.7.2&#xff0c;如下图&#xff1a; 2 分别安装TensorFlow.NET包(先装)和SciSharp.…

CleanMyMac X2024破解版下载地址链接

如果你是一位Mac用户&#xff0c;你可能会遇到一些问题&#xff0c;比如Mac运行缓慢、磁盘空间不足、应用程序难以管理等。这些问题会影响你的Mac的性能和体验&#xff0c;让你感到沮丧和无奈。那么&#xff0c;有没有一款软件可以帮助你解决这些问题呢&#xff1f;答案是肯定的…

MySQL复习总结(二):进阶篇(索引)

文章目录 一、存储引擎1.1 MySQL体系结构1.2 存储引擎介绍1.3 存储引擎特点1.4 存储引擎选择 二、索引2.1 基本介绍2.2 索引结构2.3 索引分类2.4 索引语法2.5 SQL性能分析2.6 索引使用2.6.1 最左前缀法则2.6.2 范围查询2.6.3 索引失效情况2.6.4 SQL提示2.6.5 覆盖索引2.6.6 前缀…