共计 6255 个字符,预计需要花费 16 分钟才能阅读完成。
请点赞关注,你的反对对我意义重大。
🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长常识体系,有气味相投的敌人,关注公众号 [彭旭锐] 带你建设外围竞争力。
前言
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 bytes
Space 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 bytes
Space 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 时,放弃指针压缩策略,而不是一味增大对齐填充。
6. 总结
到这里,对象的内存布局就将完了。咱们讲到了对象的调配区域、对象数据的拜访定位形式以及对象外部的布局模式。下一篇,咱们持续深刻开掘 Java 援用类型的实现原理。关注我,带你建设外围竞争力,咱们下次见。
参考资料
- 深刻了解 Java 虚拟机(第 3 版)(第 1、3、13 章)—— 周志明 著
- 深刻了解 Android:Java 虚拟机 ART(第 8.7 章 · 类的加载、链接和初始化)—— 邓凡平 著
- Java 并发编程的艺术(第 2 章 · Java 并发机制的底层实现原理)—— 方腾飞、魏鹏、程晓明 著
- JVM Anatomy Quark #23: Compressed References —— Aleksey Shipilёv 著
- JVM Anatomy Quark #24: Object Alignment —— Aleksey Shipilёv 著
你的点赞对我意义重大!微信搜寻公众号 [彭旭锐],心愿大家能够一起探讨技术,找到气味相投的敌人,咱们下次见!
享受阳光。