如何计算Java对象占堆内存中的大小

更新时间:2023-05-26 04:34:01 阅读: 评论:0

桂花雨的作者-人世间纪录片

如何计算Java对象占堆内存中的大小
2023年5月26日发(作者:太空蛋糕)

如何计算Java对象占堆内存中的⼤⼩

李强,20185⽉⼊职Qunar,现任⽕车票技术部Java开发⼯程师。参与构建⽕车票业务系统的底层技术⽀持体系,个⼈

对并发编程、分布式系统等技术点感兴趣。

前⾔

在实际⼯作中,我们可能会遇到需要计算某个对象占⽤堆内存空间⼤⼩的问题。这时,就需要我们了解 Java 对象在堆

内存中的实际存储⽅式和存储格式。本⽂就针对此问题展开分析,详细介绍了 Java 对象在堆内存中的结构,从⽽让⼤

家能够⽅便的计算出对象占⽤内存的⼤⼩。注意:本⽂默认环境为 64 位操作系统,JDK 1.8JVM HotSpot

Oop-Klass模型

我们知道 Java 对象在内存中的访问⽅式,如下图所⽰:

通过上图,我们可以知道⼀个 Java 实例对象由 Java 堆中的实例数据和⽅法区的类型数据两部分组成。对应到 JVM

部,HotSpot 设计了⼀个 Oop-Klass 的⼆分模型来表⽰⼀个 Java 实例对象的两部分:

Klass 简单来说就是 Java 类在 HotSpot 中的 C++ 对等体,主要⽤于描述对象实例的具体类型。⼀般 JVM 在加

class ⽂件时,会在⽅法区创建 Klass ,表⽰类的元数据,其包括常量池、字段、⽅法等。

Oop指的是 Ordinary Object Pointer(普通对象指针)。其在 Java 创建对象实例的时候创建,⽤于表⽰对象的实

例信息。也就是说,在 Java 应⽤程序运⾏中每创建⼀个 Java 对象,在 JVM 内部都会创建⼀个 Oop 对象来表⽰ Java

对象。这⾥有的同学可能会产⽣这样的疑问:C++ 也是⼀种⾯向对象的编程语⾔,HotSpot 为什么不将 Java 对象直接

映射成⼀个 C++ 对象,⽽是要拆分成两个呢?这是因为在 C++ 中有⼀个概念叫虚函数表,每⼀个 C++ 对象都会有⼀个

虚函数表。HotSpot 为了提⾼效率,复⽤虚函数表,才设计了这种 Oop-Klass 的⼆分模式。这个模型参考了 Smalltalk

,详细论证见 Wiki

Oop

JVM 中,Oop 的共同基类型 oopDesc 。另外根据表⽰的对象类型的不同,JVM 中具有多种 oopDesc ⼦类,每个

oopDesc 的⼦类都代表⼀个在 JVM 内部使⽤的特定对象类型。JVM Oop 主要由如下⼏种类型组成:

(详细代码见 JDK1.8 源码:./src/hotspot/share/oops/ )

1.//定义Oop的抽象基类

1.//定义Oop的抽象基类

2.typedef class oopDesc* oop;

3.//表⽰⼀个Java实例对象

4.typedef class instanceOopDesc* instanceOop;

5.//定义数组Oop的抽象基类

6.typedef class arrayOopDesc* arrayOop;

7.//表⽰Java对象数组

8.typedef class objArrayOopDesc* objArrayOop;

9.//表⽰基本类型的数组

f class typeArrayOopDesc* typeArrayOop;

例如,当我们使⽤ new 创建⼀个 Java 对象实例的时候,JVM 会创建⼀个 instanceOopDesc 对象来表⽰这个 Java

象;同理,当我们使⽤ new 创建⼀个 Java 数组实例的时候,JVM 会创建⼀个 arrayOopDes 对象来表⽰这个数组对

象。

Klass

Oop ⼀样,JVM 中定义了如下的 Klass 类型:

1.//所有Klass类的基类

2.class Klass;

3.//描述⼀个Java

4.class InstanceKlass;

5.//专有的InstanceKlass,表⽰Klass

6.class InstanceMirrorKlass;

7.//专有的InstanceKlass,⽤于类加载器

8.class InstanceClassLoaderKlass;

9.//专有的InstanceKlass,⽤于表⽰nce的⼦类的Klass

InstanceRefKlass;

11.//描述数组类型的类的基类

ArrayKlass;

13.//描述对象数组类

ObjArrayKlass;

15.//描述基本类型数组类

TypeArrayKlass;

这⾥需要额外说明⼀点:1.8 中去掉了永久代(Perm),⽽改为了元空间(MetaSpace)。因此 JDK1.8 Oop-Klass

型与 JDK1.7 存在较⼤的区别,本⽂以 JDK1.8 为准,JDK1.7 1.8 的详细区别可参考:1.71.8 Oop-Klass模型。

Java对象在堆中的布局 源码解析

上⽂我们介绍到,JVM 中通过⼀个 instanceOopDesc 对象来表⽰⼀个 Java 实例对象。想要了解 Java 对象在内存中的

布局,只需要了解 instanceOopDesc 类的结构即可。由于 instanceOopDesc 类是 oopDesc 类的⼦类,其代码⼤部分

位于 oopDesc 类中。

我们直接来看 oopDesc 类的代码:./src/hotspot/share/oops/ 中,它包含两个数据成员:

1.class oopDesc {

2.private:

3.volatile markOop _mark;

4.union _metadata {

5.Klass* _klass;

6.narrowKlass _compresd_klass;

7.} _metadata;

8.}

1._mark _mark⽤于存储对象的 HashCode、锁标记、偏向锁、⾃旋时间、分代年龄等信息。

2._metadata _metadata是⼀个指向 Klass 的指针,是⼀个共⽤体( union )。也就是说它要么是 klass 字段要么是

compresdklass

compresdklass

JVM 开启了-XX:+UCompresdClassPointers( 表⽰启⽤ Klass 指针压缩选项, JVM 默认开启 )选项时使⽤

commpresdklass 来存储 Klass 引⽤了,否则使⽤ _klass 存储 Klass 引⽤。

注意:在数组相关的 Oop 类中,除了上述两个数据成员外,还有⼀个 int 类型数据成员 length ,⽤于表⽰数组的长度。

详细代码见:./src/hotspot/share/oops/ oopDesc 类本⾝的数据成员,我们称之为对象头。除了对象头

之外,oopDesc 还需要保存 Java 对象的实例字段。这些实例字段紧跟对象头存储,其起始偏移地址可通过

instanceOopDesc 类中的函数( 位于:./src/hotspot/share/oops/)计算得出:

1.// If compresd, the offt of the fields of the instance may not be aligned.

2.static int ba_offt_in_bytes() {

3.// offt computation code breaks if UCompresdClassPointers

4.// only is true

5.return (UCompresdOops && UCompresdClassPointers) ?

6.klass_gap_offt_in_bytes() :

7.sizeof(instanceOopDesc);

8.}

9.//./src/hotspot/share/oops/

int klass_gap_offt_in_bytes() {

(has_klass_gap(), "only applicable to compresd klass pointers");

klass_offt_in_bytes() + sizeof(narrowKlass);

13.}

14.

int klass_offt_in_bytes() { return offt_of(oopDesc, _metadata._klass); }

⽰例

通过对源码的分析,我们基本弄清了 Java 对象在 JVM 内部的内存分布,下⾯我们通过⼀个例⼦来更直观地说明。

如有如下代码:

1.public class Model {

2.

3.public static int a = 1;

4.

5.public int b;

6.

7.public Model(int b) {

8.this.b = b;

9.}

10.

11.}

12.

static void main(String[] args) {

14.

c = 10;

modela = new Model(2);

17.

modelb = new Model(3);

19.}

那么其在内存中的布局如下图所⽰:

总结

根据上⽂的学习,我们可以总结出⼀个 Java 对象在堆内存中的布局⼤致如下所⽰:

注意:这⾥的 Padding ( 对齐填充 )是作为填充字段,为满⾜ Java 对象所占内存必须为 8 字节的倍数⽽存在的。

1.对象头:对象头是 Java 对象存储结构中最复杂的部分。它由下述⼏部分组成:

(1)mark word:在 64 位系统下⽤ 8 字节表⽰;32 位系统为 4 字节。

(2)metadata64 位系统下,若 JVM 开启 Klass 指针压缩选项( -XX:+UCompresdClassPointersJVM 默认开启

此选项 ),则⽤ 4 字节表⽰;若不开启指针压缩( -XX:-UCompresdOops )则⽤ 8 字节表⽰;32 位系统则使⽤ 4

字节表⽰。 指针压缩的⽬的就是为了节省内存,若有同学对具体的压缩算法感兴趣可参考:CompresdOops

(3)Length:若当前对象为数组,那么对象头中除了上述两部分内容外,还会有 4 字节的内容⽤于表⽰存储数组的长度

信息;若当前对象不为数组,则对象头中不存在此项信息。

2.实例数据:实例数据中存储的就是当前对象的实例字段。字段的存储类型有基本类型和引⽤类型两种,它们对应的存

储⼤⼩如下图:

其中 ref 表⽰引⽤类型,引⽤类型实际上是⼀个地址指针,其在 32 位系统中,占⽤ 4 字节,64 位系统中,如果开启了

指针压缩( -XX:+UCompresdOops JVM 默认开启此选项 )则占⽤ 4 字节,若不开启则占⽤ 8 字节。 此外,实例

数据中的字段既包括从⽗类继承的,也包括⼦类本⾝定义的。这些字段在内存中的存储顺序会受到虚拟机分配策略参数

FieldsAllocationStyle )和字段在 Java 源码中定义顺序的影响。HotSpot 中有三种虚拟机分配策略,见如下代码注

释(源码位置:./hotspot/share/classfile/):

1.// Rearrange fields for a given allocation style

2.if( allocation_style == 0 ) {

3.// Fields order: oops, longs/doubles, ints, shorts/chars, bytes, padded fields

4.......

5.} el if( allocation_style == 1 ) {

6.// Fields order: longs/doubles, ints, shorts/chars, bytes, oops, padded fields

7.......

8.} el if( allocation_style == 2 ) {

9.// Fields allocation: oops fields in super and sub class are together.

10.......

11.}

从中可以看出这三种策略的字段排列顺序不同:

1.策略0oops ( Ordinary Object Pointers ,普通对象指针,也就是引⽤类型 )在基本数据类型前⾯, 其后依次是

longs/doubles, ints, shorts/chars, bytes , 最后是填充字段, 以满⾜对齐要求。

2.策略1oops 在基本数据类型之后。

3.策略2:⽗类中的引⽤类型与⼦类中的引⽤类型放在⼀起。JVM 默认分配策略为 1 ,可通过参数 -

XX:FieldsAllocationStyle=2 ,将分配策略变更为 2 。策略 0 和策略 1 区别不⼤,都是将基本数据类型按照从⼤到⼩的

⽅式排序,这样可以降低空间开销。⽽策略 3 将⽗类和⼦类的引⽤放在⼀起可以增加 GC 效率,试想在 GC 扫描引⽤

时,由于⽗类和⼦类的引⽤连续,可能只需要扫描⼀个 cache line 即可,若⽗类和⼦类的引⽤不连续,则需要扫描多个

cache line ;另外连续的内存引⽤还可减少 OopMap 的个数,从⽽达到提⾼ GC 效率的⽬的。关于虚拟机内存分配策略

的详细代码解读可参考:jvm源码分析之oop-klass对象模型,这⾥不再展开细说。在满⾜上述的虚拟机分配策略前提条

件下,⼀般⽗类的字段会在⼦类的字段之前,但是 JVM 也提供了参数 -XX:+/-CompactFields ( 默认开启 ),来允许将⼦

类实例字段中的⼩对象插⼊到⽗类变量的缝隙中。 总体来看,实例数据的⼤⼩就是对象的各个实例字段的⼤⼩之和。

对齐填充:JVM 要求对象的⼤⼩必须是 8 字节的整数倍,因此当对象头+实例数据的⼤⼩不满⾜ 8 字节的整数倍时,

就需要增加填充数据,以满⾜此条件。按照 8 字节对齐,是底层 CPU 数据总线读取内存数据的要求。通常 CPU 按照

字长来读取数据,⼀个数据若不对齐则可能需要 CPU 读取两次,若进⾏了对齐,则⼀次性即可取出⽬标数据,这将会

⼤⼤节省 CPU 资源,因此对象⼤⼩需要对齐。

通过上⾯的介绍,我们基本就可以计算出⼀个 Java 对象占⽤堆内存的⼤⼩了。假如有如下代码:

1.public class People {

2.int age = 20;

3.

4.String name = "XiaoMing";

5.}

6.public class Person extends People {

7.boolean married = fal;

8.

9.long birthday = 3283520940L;

10.

tag = 'a';

12.

sallary = 4700.00d;

14.}

那么⼀个 Person 对象的堆内存⼤⼩可以计算得出:

对象头:8( mark )+4( Klass 指针 )=12

对象头:8( mark )+4( Klass 指针 )=12

实例数据:4( age )+4( name )+1( married )+8( birthday )+2( tag )+8( sallay )=27那么此时 Person 对象的⼤⼩为:

12+27+1( padding )=40 此时计算的 name 是⼀个指向 String 对象的指针,我们还需要加上 name 对象的⼤⼩:8(

mark )+4( Klass 指针 )+4( hash )+4( value[] )+4( padding )=24 其中 value[] 是⼀个 char 数组的指针,因此需要再加上

此数组的长度:8( mark )+4( Klass 指针 )+4( length )+8*2( 8 char )+0( padding )=32。综上,⼀个 Person 对象占⽤堆

内存的⼤⼩为 40+24+32=96 字节。

HSDB

通过上⼀章节的学习,我们已经 get 到如何计算⼀个 Java 对象占⽤的堆内存⼤⼩,但是如何来验证我们计算的是否正

确呢?其实 HotSpot 已经为我们提供了⼀个⼯具来查询运⾏时对象的 Oops 结构,那就是 HSDB

1.我们在命令⾏中运⾏如下程序:

1.public static void main(String[] args) throws InterruptedException {

2.

3.Person person = new Person();

4.

5.(60*1000);

6.

7.n("end");

8.}

2.使⽤ JPS 获取运⾏的 Java 进程号,运⾏如下指令启动 HSDB sudo java -cp $JAVA_HOME/lib/

,然后选择 File->Attach to HotSpot process并输⼊进程 ID

3.此时会显⽰对应进程的线程信息。点击Tools->Object Histogram即可打开堆的对象列表。在列表中输⼊想要查看的类

名称进⾏搜索:

双击对应的对象,然后点击 Inspect 按钮即可看到该对象的 Oop 结构信息:

4.也可以点击 Tools->Class Browr打开类信息列表,并搜索想查看类信息,也可对象中各个字段的偏移量。

代码计算内存占⽤的⽅法

前⽂我们介绍了 Java 对象在堆中的内存布局之后,就可⼿动计算出⼀个 Java 对象占⽤的堆内存⼤⼩,但是假如我们需

要在代码中计算⼀个对象的内存占⽤⼜该如何进⾏呢?这⾥总结了三种⽅式供⼤家参考:

Instrumentation

使⽤ectSize()⽅法可以⽅便地计算出⼀个运⾏时对象的⼤⼩。关于如何获取

Instrumentation 这⾥不再赘述,我们关注⼀下 getObjectSize()⽅法的注释:

1./**

2.* Returns an implementation-specific approximation of the amount of storage consumed by

3.* the specified object. The result may include some or all of the object's overhead,

4.* and thus is uful for comparison within an implementation but not between implementations.

5.*

6.* The estimate may change during a single invocation of the JVM.

7.*

8.* @param objectToSize the object to size

9.* @return an implementation-specific approximation of the amount of storage consumed by the specified object

10.* @throws interException if the supplied Object is null.

11.*/

getObjectSize(Object objectToSize);

通过注释我们可知,此⽅法求出的值是⼀个近似值,并不准确。因此这种⽅法只适⽤于同⼀个对象多次求⼤⼩并进⾏⽐

较,不适⽤⽤两个对象之间⽐较。

使⽤Unsafe

java 中的 类,有⼀个 objectFieldOfft(Field f) ⽅法,表⽰获取指定字段在所在实例中的起始地址偏移

量。因此我们可以获取指定的对象中每个字段的偏移量,并求出其中的最⼤值。偏移量最⼤的字段肯定位于实例数据中

的最后,再使⽤该字段的偏移量加上该字段的实际⼤⼩,就能知道该对象整体的⼤⼩。 例如有如下类:

1.public class Example {

2.int size = 20;

3.String name = "XiaoMing";

4.}

使⽤如下代码计算其字段的偏移地址:

使⽤如下代码计算其字段的偏移地址:

1.public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

2.Field field = laredField("theUnsafe");

3.essible(true);

4.Unsafe unsafe = (Unsafe) (null);

5.

6.Example example = new Example();

7.

8.Field sizeField = ss().getDeclaredField("size");

9.long sizeAddr = FieldOfft(sizeField);

n("size offt is:" + sizeAddr);

11.

nameField = ss().getDeclaredField("name");

nameAddr = FieldOfft(nameField);

n("name offt is:" + nameAddr);

15.}

结果:

1.size offt is:12

2.name offt is:16

由上可知 name offt 最⼤为 16 ,在加上其本⾝的长度 4 ( String 对象的指针 ),则 Example 对象的实际⼤⼩为:

16+4+4( Padding )=24

但是上述这种⽅式计算的只能是对象本⾝的⼤⼩,并没有计算对象中的引⽤类型所引⽤的对象的⼤⼩。若想要获取⼀个

对象的完整⼤⼩,则还需要写代码进⾏递归计算。

使⽤第三⽅⼯具

这⾥提供⼀个第三⽅专门计算堆内存占⽤⼤⼩的⼯具类:

1.

2.arch

3.java-sizeof

4.0.0.5

5.

其中 RamUsageEstimator类提供获取对象堆内存占⽤⼤⼩的⽅法,如下所⽰验证我们前⼀章节计算的对象⼤⼩:

1.public static void main(String[] args) {

2.People people = new People();

3.

4.Person person = new Person();

5.

6.n("people:"+(people));

7.n("person:"+(person));

8.

9.}

10.结果:

:80

:96

RamUsageEstimator 类主要就是根据 JVM 规范计算对象的⼤⼩,并不是根据实际的内存地址计算。因此,它存在⼀个

缺点,那就是可能存在与实际⼤⼩不符合的情况。

参考资料

1.《深⼊理解 Java 虚拟机》第2版,周志明,机械⼯业出版社;

2. Hotspot oopsklass handle;

3. 如何计算 Java 对象所占内存的⼤⼩;

4. Hotspot GC研究- 开篇&对象内存布局;

5. Java 对象内存结构。

总结

本⽂⾸先介绍了 JVM Oop-Klass 模型,这是理解 Java 内存布局的基础,接下来讲解了⼀个 Java 对象在堆中的结

构,然后⼜简单介绍了如何通过 HSDB 查看对象的 Oop 结构,最后介绍了⼏种通过代码计算 Java 对象内存布局的⽅

式。 掌握对象的内存布局是解决⼯作中遇到的⼀些诸如:评估本地内存的占⽤量、定位内存泄漏 Bug 等问题的基础。

希望⼤家通过阅读本⽂能够有所收获。 本⽂参考了多⽅⾯资料,并做了⼀些总结,可能会有些不正确的地⽅敬请指正。

会计简历模板-打工仔买房记

如何计算Java对象占堆内存中的大小

本文发布于:2023-05-26 04:34:00,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/zhishi/a/1685046841179303.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

本文word下载地址:如何计算Java对象占堆内存中的大小.doc

本文 PDF 下载地址:如何计算Java对象占堆内存中的大小.pdf

标签:小对象
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 实用文体写作网旗下知识大全大全栏目是一个全百科类宝库! 优秀范文|法律文书|专利查询|