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

这篇具有很好参考价值的文章主要介绍了一看就懂的java对象内存布局。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

Java 中一切皆对象,同时对象也是 Java 编程中接触最多的概念,深入理解 Java 对象能够更帮助我们深入地掌握 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对象内存布局,java,java

句柄访问:

关于 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 字节的倍数,则会在对象末尾增加对齐填充。

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

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

4. 对象内存布局详解

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

4.1 对象头(Header)**

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

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

  • 类型指针(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 策略,那么子类较窄的字段有可能插入到父类变量的空隙中。

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

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 内存空间。

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

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

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

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

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

到了这里,关于一看就懂的java对象内存布局的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 一看就懂的OpenGL ES教程——仿抖音滤镜的各种奇技淫巧(一)_opengl es添加视频(1)

    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。 深知大多数HarmonyOS鸿蒙开发工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学

    2024年04月16日
    浏览(34)
  • 【C/C++】结构体内存对齐 ----- 面试必考(超详细解析,小白一看就懂!!!)

    【C/C++】结构体内存对齐 ----- 面试必考(超详细解析,小白一看就懂!!!),C++,C语言超详细解析,c语言,c++,面试,linux,算法

    2024年03月09日
    浏览(39)
  • 动态内存分配:malloc、calloc、realloc(超详解析,多维度分析,小白一看就懂!!!!)

    目录 一、前言 二、动态内存的简单介绍 🍉什么是动态内存分配 🍎为什么要使用动态内存分配  三、动态内存函数的介绍和拓展  🍋malloc()函数  🍊free()函数  🍌calloc()函数 🍇realloc()函数 四、常见动态内存分配的错误 五、共勉 在学习 动态内存分配 时,感觉这些动态分

    2024年02月05日
    浏览(38)
  • java三层架构,有图有案例有代码,一看就懂!!!

    三层架构 三层结构解释: 视图层:主要是用于与用户进行交互,比如接收用户输入的内容将返回结果向用户展示等。 业务逻辑层:实现每个功能的特定的逻辑方法。 数据访问层:主要是与数据库进行连接,然后对数据库进行增删改查工作。 结构一: 包的层级结构: 三层结

    2024年02月03日
    浏览(36)
  • python文件操作(一看就懂)

    现在我们接着上文讲,如果没有看过我上篇文章的萌新朋友们可以先去看看,不然一会儿容易读的你满脸问号。python字符编码 现在我们来思考一个问题,我们在PyCharm里敲字符,这个字符应当先存到内存中去,而内存里都存的是unicode格式的二进制,如果我们现在就把他存到硬

    2024年02月03日
    浏览(31)
  • 插入排序超详解释,一看就懂

    目录 一、插入排序的相关概念 1、基本思想 2、基本操作:有序插入 二、插入排序的种类 三、直接插入排序 1、直接插入排序的过程:顺序查找法查找插入位置 2、使用“哨兵”直接插入排序 四、 直接插入排序算法描述 五、折半插入排序 1、查找插入位置时采用折半查找法,

    2024年01月25日
    浏览(33)
  • 【数据结构】插入排序详细图解(一看就懂)

      💯 博客内容:【数据结构】插入排序详细图解(一看就懂) 😀 作  者:陈大大陈 🦉所属专栏:数据结构笔记 🚀 个人简介:一个正在努力学技术的准前端,专注基础和实战分享 ,欢迎私信! 💖 欢迎大家:这里是CSDN,我总结知识和写笔记的地方,喜欢的话请三连,

    2024年02月07日
    浏览(39)
  • C# for循环案例1 (一看就懂)

     效果:  用f10可以清楚的看到程序执行的每一步,  #region for循环 /*   语法:     for(表达式1;表达式2;表达式3) {    循环体; } for(表达式1;表达式2;表达式3)   表达式1 定义循环的次数,可以理解为循环变量。表达式2 执行的条件。 表达式3,改变循环的条件,使循环条件不

    2023年04月11日
    浏览(37)
  • 微信小程序实现倒计时功能,一看就懂,直接用

    结构完整,直接用就可以

    2024年02月01日
    浏览(51)
  • 如何在android运行lua脚本(最简单的讲解,一看就懂)

    1.打开 android studio 2.引入luaj-jse-3.0.1.jar包(百度自行下载) 3.新建assets文件夹 4.在assets文件夹下新建一个 main.lua文件,内容如下 5.MainActivity.java 内容如下 5.运行程序

    2024年02月11日
    浏览(26)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包