java对象的创建实验总结_Java 虚拟机 | 拿放大镜看对象

bceb8e34b27c

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 「Java 路线」导读 —— 他山之石,可以攻玉 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

目录

bceb8e34b27c

前置知识

这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~

1. 对象的创建过程

在 Java 中创建对象的一般方式是使用 new 关键字,编译后会生成以 new 字节码指令开始的多条指令,例如:

源代码:

String str = new String();

字节码:

0 new #26

3 dup

4 invokespecial #27 >

7 astore_0

bceb8e34b27c

—— 图片引用自网络

提示: 这里讨论的对象是指一般的对象,即使用 new 创建的对象。

1.1 检查加载 & 类加载

根据常量池索引#26找到类的符号引用,并且检查类是否被类加载器加载过。如果没有,那么必须先执行相应的类加载过程(加载、解析和初始化)。

1.2 分配内存

1.2.1 分配方式

对象需要一块连续的内存空间,在堆中划分内存的方法有 指针碰撞 & 空闲列表 。使用哪种分配方法取决于 Java 堆是否 规整,而 Java 堆是否规整由取决于垃圾回收器算法是否具备 整理 功能。

指针碰撞

指针碰撞要求 Java 堆是绝对规整的,即:所有已分配内存压缩到堆的一端,剩下一端为空闲的内存,两块区域使用一个 分配指针 作为分界指示器。当需要分配对象内存时,只需要把指针向挪动与对象大小相等的距离,将该区域划分给对象。

空闲列表

空闲列表不要求 Java 堆是绝对规整的,虚拟机会维护一个列表记录哪些内存时空闲的。当需要对象内存时,需要遍历空闲列表找到一块足够大的空间划分给对象。

1.2.2 并发安全

由于 Java 堆是线程共享的,而创建对象(分配内存)的行为在虚拟机中是非常频繁的,那么就需要考虑不同线程并发操作指针的安全问题,解决方法有:CAS 操作 & 分配缓冲:

CAS 操作

采用自旋 CAS 操作实现更改指针操作的线程安全性;

TLAB 分配缓冲

每个线程在 Java 堆中预先分配一小块内存,即 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),让每个线程使用专属的分配指针来分配空间,其他线程无法在这个区域中分配,这样就较少了线程同步开销。

通过虚拟机参数-XX+UseTLAB来控制是否启用 TLAB 功能。

提示: TLAB 中的中的对象空间依然是所有线程共享的,只是其他线程无法在这个区域分配对象。

1.3 初始化零值

将分配的内存空间初始化为零值,例如 int 为 0 ,boolean 为 false,引用类型为 null。

1.4 设置

设置对象的类元数据信息,对象头等信息。

1.5 构造函数

执行构造函数 , 不但指对象的构造函数,还包括在成员变量上的初始值和实例代码块。

2. 对象的内存布局

在 HotSpot 虚拟机中,对象的内存区域可以划分为:

对象头:(Mark Work & 类型指针 & 数组长度)

实例数据

对齐填充

bceb8e34b27c

2.1 对象头(Header)

对象头包含 Mark Work & 类型指针 & 数组长度。

2.1.1 Mark Work

由于对象头里的信息是与对象实例数据无关的额外存储成本,Mark Word 被设计为一个有状态的数据结构,可以根据对象的状态 复用。

bceb8e34b27c

2.1.2 类型指针(Class Pointer)

定义: 指向方法区中的类型元数据,可选,取决于对象的访问定位方式;

长度: 在 32 位机器上占用 4 个字节,在 64 位机器上占 8 个字节。虚拟机(默认)通过 指针压缩 将长度压缩到 4 个字节,通过以下虚拟机参数控制。

-XX:+UseCompressedClassPointers -XX:+UseCompressedOops

注意: 并不是所有虚拟机实现都将类型指针存在对象数据上。具体取决于虚拟机使用的 对象的访问定位 方式,如果是使用 直接指针 的方式,对象的内存布局就必须放置访问类型数据的指针。

2.1.3 数组长度

定义: 指数组对象的长度,注意这里的长度指的是元素个数,非占用内存空间(可选,只有数组对象才有);

长度: 4 个字节;

描述: 普通 Java 对象的大小可以通过元数据信息确定,但是对于数组对象来说,无法通过元数据的信息确定数组的长度。因此,如果对象是一个Java数组,那么对象头中会有一块记录数组长度的区域。例如:

源码:

char [] str = new char[2];

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

------------------------------------------------------

JOL:

[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. 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,表示该数组长度为 2。

2.2 实例数据(Instance Data)

实例数据是对象的有效信息,可以理解为报文段中的 payload。对象的实例数据包括:

本类声明的实例字段

从父类继承的实例字段

但不包括类级字段(存储在方法区)。

2.3 对齐填充(Padding)

HotSpot 虚拟机对象的大小必须按 8 字节对齐,如果对象占用空间不是 8 字节的倍数,则需要增加对齐填充数据。直观来看,“无效” 的填充数据使得对象占用空间加大,增大了虚拟机的内存消耗。那么为什么要这么做呢? Editting...

2.4 实验

JOL(Java Object Layout)是 OpenJDK 提供的用于分析对象内存布局的工具,地址:JOL。主要的局限性是只支持 HotSpot / OpenJDK 虚拟机,如果在其他虚拟机上使用会报错:

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

现在,我们使用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类型占用的长度,见源码:

HotspotUnsafe.java

public String details() {

// ...

out.printf("# %-19s: %d, %d, %d, %d, %d, %d, %d, %d, %d [bytes]%n",

"Field sizes by type",

oopSize,

sizes.booleanSize,

sizes.byteSize,

sizes.charSize,

sizes.shortSize,

sizes.intSize,

sizes.floatSize,

sizes.longSize,

sizes.doubleSize

);

}

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

3. 对象的访问定位

我们都知道 Java 的类型可以分为基础数据类型与引用类型(Reference)。对于引用类型变量,在虚拟机栈上存储的只是 Reference,而对象真正的实例数据是存储在堆上。通过 Reference 访问对象实例数据的方式分为分为 句柄访问 & 直接指针访问:

3.1 句柄访问

在 Java 堆中单独划分一块区域作为句柄池,Reference 中存储是对象的句柄。句柄中存储的是对象实例数据与类型数据的地址。

句柄访问的优点是句柄中对象实例数据和类型数据的地址是稳定的,当对象在垃圾收集是被移动时,只需要修改实例数据的指针,而 Reference 本身不需要修改。

bceb8e34b27c

引用自《深入理解Java虚拟机(第3版本)》—— 周志明 著

3.2 直接指针访问

Reference 中存储的是指向对象的地址,对象内存中有一块是实例数据,另外有一个指针指向类型数据,这个指针就是 第 2.1.2 节 中的类型指针(Class Pointer)

直接指针访问的优点是速度更快,因为节省了一次指针的访问。由于在 Java 虚拟机中对象访问的频率非常高,所以直接指针访问的优势更明显。

bceb8e34b27c

引用自《深入理解Java虚拟机(第3版本)》—— 周志明 著

4. 对象的存活判断

判断对象是否为垃圾对象的方法可以分为两种:引用计数 & 可达性分析。

4.1 引用计数算法(Reference Counting)

在分配对象时,会额外为对象分配一段空间,用于记录指向该对象的引用个数。如果有一个新的引用指向该对象,则计数器加 1;当一个引用不再指向该对象,则计数器减 1 。当计数器的值为 0 时,则该对象为垃圾对象。

优点:

1、及时性:当对象变成垃圾后,程序可以立刻感知,马上回收;而在可达性分析算法中,直到执行 GC 才能感知;

2、增量回收:GC 可与应用交替运行,最大暂停时间短。

缺点:

1、计数器值更新频繁:大多数情况下,对象的引用状态会频繁更新,更新计数器值的任务会变得繁重;

2、堆利用率降低:计数器至少占用 32 位空间(取决于机器位数),导致堆的利用率降低;

3、实现复杂;

4、(致命缺陷)无法回收循环引用对象。

易错: 引用计数法是算法简单,实现较难。

4.2 可达性分析算法(Reachability Analysis)

从 GC 根节点(GC Root)为起点,根据引用关系形成引用链。当一个对象存在到 GC Root 的引用链,则为存活对象,否则为垃圾对象。在 Java 中,GC Root 主要包括:

1、Java 虚拟机栈中引用的对象(即栈帧中的本地变量表);

2、本地方法栈中引用的对象;

3、方法区中类静态变量引用的对象;

4、方法区常量池中引用的对象;

5、同步锁(synchronized 关键字)持有的对象;

优点:

1、可回收循环引用对象;

2、实现简单。

缺点:

1、最大停顿时间长:在 GC 期间,整个应用停顿(stop-the-world,STW);

2、回收不及时:直到执行 GC 才能感知垃圾对象;

4.3 小结

判定方法

优点

缺点

引用计数

1、及时性

2、增量回收

1、计数器值更新频繁

2、堆利用率降低

3、实现复杂

4、无法回收循环引用对象

可达性分析

1、可回收循环引用对象

2、实现简单

1、最大停顿时间长

2、回收不及时

由于引用计数式 GC 存在「无法回收循环引用对象」 的致命缺陷,工业实现上还是追踪式 GC 占据了主流,后面我主要介绍的也是追踪式 GC。

5. 对象的引用类型

不同引用类型的作用不尽相同,这一点很多文章没有明确指出。软引用 & 弱引用提供了更加灵活地控制对象生存期的能力,而虚引用提供了感知对象垃圾回收的能力。 除了虚引用之外,Object#finalize() 也提供了感知对象被垃圾回收的能力。

引用类型

Class

作用

对象 GC 时机(不考虑 GC 策略)

强引用

/

GC Root 可达就不会回收

软引用

SoftReference

灵活控制生存期

空闲内存不足以分配新对象时

弱引用

WeakReference

灵活控制生存期

每次GC

虚引用

PhantomReference

感知对象垃圾回收

每次GC

提示: 对象是否被 GC,不仅仅取决于引用类型,还取决于当次 GC 采用的策略。

6. 对象的分配策略

6.1 对象的分配区域

几乎所有对象都分配在 Java 堆,除此之外还可以分配在:

方法区:Class 对象、字符串常量池中的 String

栈:满足逃逸分析的对象直接在栈上分配

6.2 逃逸分析

逃逸分析(Escape Analysis)是分析对象的引用是否逃逸到当前栈帧或者其它线程,如果一个对象不会逃逸,则可以直接在栈上分配,而不是分配在 Java 堆。当对象在栈上分配时,当前方法结束之后对象的生命周期也结束了,不需要参与垃圾回收,可以提高虚拟机的执行效率。

通过JVM参数可指定是否开启逃逸分析:-XX:+DoEscapeAnalysis

6.3 对象的分配原则

1、对象优先在 Eden 区分配

大多数情况下,新生对象在 Eden 区分配,当 Eden 区没有足够空间时,虚拟机发起一次 Minor GC。

2、大对象直接在 Tenured 区分配

大对象占用内容较多,如果分配在 Eden 区的话,容易提前发生垃圾回收,同时 GC 的时候也会大量复制内存,所以大对象直接在 Tenured 区分配。

3、对象年龄动态提升

在对象头中有一个字段标记对象的年龄,如果对象经过一次 Minor GC 之后依然存活,并且 Survivor 区能够容纳的话,那么对象会被复制到 Survivor 区,并且对象的年龄加 1。当对象的年龄增加到一定程度时,就是晋升到 Tenured 区。

参考资料

《深入理解Java虚拟机(第3版本)》(第2、3、13章)—— 周志明 著

《Android进阶解密》(第10章)—— 刘望舒 著

《Java并发编程的艺术》​(第2、6章)—— 方腾飞、魏鹏、程晓明 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

bceb8e34b27c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值