Java虚拟机--你的对象有多大如何计算对象大小

如何计算对象大小

上文中,笔者提到了对象头,并且说到了对象头中的Mark Word在32位的机器中会占用4字节,在64位机器中占用8字节。那么,整个对象会占用多大内存呢?

带着这样的疑问,我们来实际的测量下,一个对象到底会占用多大内存?

在实际计算之前,我们先来普及下接口Instrumentation和其实现类InstrumentationImpl。

Instrumentation介绍:

java.lang.instrument.Instrumentation接口:它提供了丰富的对结构的等各方面的跟踪和对象大小的测量的API。

sun.instrument.IntrumentationImpl类:sun开头的,Instrumentation接口的实现类,构造方法为private,没有任何getInstance的方法。

使用Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在JVM上的程序。开发者就可以实现更为灵活的运行时虚拟机监控,这样的特性实际上提供了一种虚拟机级别支持的AOP实现方式,使得开发者无需对JDK做任何升级和改动,就可以实现某些AOP的功能了。

说的直白点,Instrumentation就是一个代理。在代码层面,java.lang.instrument.Instrumentation是接口,sun.instrument.InstrumentationImpl是其实现类。

说了这么多,那么Instrumentation应该怎么使用呢?

值得一体的是,Instrumentation不能像我们平常new对象的方式来实现使用,代码层面我们无法得到Instrumentation的实例对象。开发者需要提供premain函数,让虚拟机注入。此外,premain函数在 main函数运行前执行。简要说来就是如下几个步骤:

(1)编写premain函数

编写一个 Java 类,包含如下两个方法当中的任何一个
1. public static void premain(String agentArgs, Instrumentation inst); 
2. public static void premain(String agentArgs); 
其中,1 的优先级比 2 高,将会被优先执行(1 和 2 同时存在时,2 被忽略)。

在这个premain函数中,开发者可以进行对类的各种操作。inst是java.lang.instrument.Instrumentation 的实例,由JVM自动传入。

java.lang.instrument.Instrumentation是instrument包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和类的操作等。

(2)class文件打成jar包

将这个Java类编译成class文件,再打成一个jar包,并在jar包中META-INF/MANIFEST.MF文件加入“ Premain-Class”来指定步骤1中编写的那个带有premain的Java类。

Premain-Class: java类的全限定类名

(3)运行

用命令行中输入如下命令:

java -javaagent:xxx.jar 被代理的类

说完了Instrumentation,接下来就用它来实际测量下对象的大小:

普通对象:

Instrumentation注入类:

public class ObjectSize {

    private static Instrumentation inst;

    public static void premain(String agentArgs, Instrumentation instP){
        inst = instP;
    }

    public static long sizeOf(Object obj){
        return inst.getObjectSize(obj);
    }
}

java.lang.instrument.Instrumentation.getObjectSize()的方式,这种方法得到的是Shallow Size,即遇到引用时,只计算引用的长度,不计算所引用的对象的实际大小。如果要计算所引用对象的实际大小,可以通过递归的方式去计算。

编写测试类:

public class JVMTest4 {

    private static class ObjectA {
        String str;  
        int i1; 
        byte b1; 
        byte b2; 
        int i2;  
        byte b3;  
    }

    public static void main(String[] args){
        System.out.println(ObjectSize.sizeOf(new ObjectA()));
    }
}

运行程序:

[图片上传失败...(image-c1c0a4-1525935829095)]

将打包好的jar文件,用解压缩工具打开,修改META-INF/MANIFEST.MF文件,告诉虚拟机在程序执行的时候执行ObjectSize类的permain方法,从名称也可以看出,含义为:“在main方法之前执行”。

编译运行

此步骤,是实际的运行过程,需要将上面的2个类进行编译,并且将ObjectSize打包,执行“java -javaagent:ObjectSize.jar JVMTest4”命令。

从截图中,我们可以看出ObjectA对象在内存中占用了32个字节。

上文中说了。对象的大小为8的倍数,如果不足8的倍数则会进行对齐填充。

下面,我们来手动计算下(64位机器,默认开启指针压缩)

原生类型

占用内存大小(字节)

boolean

1

byte

1

short

2

char

2

int

4

float

4

long

8

double

8

reference

开启指针压缩4、关闭指针压缩8

对象引用(reference)类型在64位机器上,关闭指针压缩时占用8字节, 开启时占用4字节。

实例数据:str(4)+i1(4)+b1(1)+b2(1)+i2(4)+b3(1) = 15字节

对象头:8(Mark Word) + 4(类型指针) = 12字节

对齐填充:5字节

总计:15 + 12 + 5 = 32字节

关闭指针压缩情况下:使用-XX:-UseCompressedOops命令

实例数据:str(8)+i1(4)+b1(1)+b2(1)+i2(4)+b3(1) = 19字节

对象头:8(Mark Word) + 8(类型指针) = 16字节

对齐填充:5字节

总计:19 + 16 + 5 = 40字节

数组对象:

Instrumentation注入类:

public class ObjectSize {

    private static Instrumentation inst;

    public static void premain(String agentArgs, Instrumentation instP){
        inst = instP;
    }

    public static long sizeOf(Object obj){
        return inst.getObjectSize(obj);
    }
}

编写测试类:

public class JVMTest4 {

    private static class ObjectA {
        String str;  
        int i1; 
        byte b1; 
        byte b2; 
    }

    private static class ObjectB {
        
    }

    public static void main(String[] args){
        System.out.println(ObjectSize.sizeOf(new ObjectA[0]));
        System.out.println(ObjectSize.sizeOf(new ObjectA[1]));
        System.out.println(ObjectSize.sizeOf(new ObjectA[2]));

        System.out.println(ObjectSize.sizeOf(new ObjectB[0]));
        System.out.println(ObjectSize.sizeOf(new ObjectB[1]));
        System.out.println(ObjectSize.sizeOf(new ObjectB[2]));
    }
}

运行程序:

image

从测试结果来看,数组对象要比普通对象占用内存空间更大。值得注意的是,数组占用内存的大小并不会根据成员变量的增加而增大。无论是否存在成员变量,都不会影响数组对象占用内存的大小。

你可能还有个疑惑?例子中的数组只设置了长度,而没有实际赋值对象,如果向对应的角标下赋值,数组对象占用内存的大小会有变化吗?

答案:NO!!

数组对象占用内存大小公式:

Mark Word + 类型指针 + 数组长度 + 实例数据(数组长度*数组元数据大小) +补齐填充

数组与普通对象不同之处,在与其实例数据部分。对于普通对象来说,实例数据就是其内部的成员变量;而对于数组来说,实例数据就是其内部的一个个原生对象,而原生对象所占用内存大小在开启指针压缩情况下为4字节,关闭指针压缩情况下为8字节。

开启指针压缩:

mark word 8  + 类型指针 4 + 数组长度 4 + 0*4 + 补齐 0  = 16

mark word 8  + 类型指针 4 + 数组长度 4 + 1*4 + 补齐 4  = 24

mark word 8  + 类型指针 4 + 数组长度 4 + 2*4 + 补齐 0  = 24

mark word 8  + 类型指针 4 + 数组长度 4 + 3*4 + 补齐 0  = 32

未开启指针压缩:

mark word 8  + 类型指针 8 + 数组长度 4 + 0*8 +  + 补齐 0  = 24

mark word 8  + 类型指针 8 + 数组长度 4 + 1*8 + 补齐 4  = 32

mark word 8  + 类型指针 8 + 数组长度 4 + 2*8 + 补齐 4  = 40

mark word 8  + 类型指针 8 + 数组长度 4 + 3*8 +  + 补齐 4  = 56

再次强调下:

我们例子中调用的getObjectSize()方法得到的是Shallow Size,即遇到引用时,只计算引用的长度,不计算所引用对象的实际大小。如果要计算所引用对象的实际大小,可以通过递归的方式去计算。本文暂不介绍此方式,有兴趣的朋友可以去网上查阅相关资料。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏海天一树

小朋友学Python(19):异常

一、什么是异常 异常即是一个事件,该事件会在程序执行过程中发生,影响了程序的正常执行。 一般情况下,在Python无法正常处理程序时就会发生一个异常。 异常是P...

3389
来自专栏佳爷的后花媛

java基础知识

Vector、Stack、HashTable、ConcurrentHashMap、Properties

3015
来自专栏PHP实战技术

PHP垃圾回收机制

1、每一个变量定义时都保存在一个叫zval的容器里面,这里面包含了数量的类型和和值,还包含了一个refcount(理解为存在几个变量个数)和is_ref(理解为...

4055
来自专栏Java技术栈

IntegerCache的妙用和陷阱!

考虑下面的小程序,你认为会输出为什么结果? public class Test { public static void main(String[...

3565
来自专栏Linux驱动

C语言异常处理之 setjmp()和longjmp()

异常处理之除0情况 相信大家处理除0时,都会通过函数,然后判断除数是否为0,代码如下所示: double divide(doublea,double b) { ...

3454
来自专栏情情说

深入浅出MyBatis:反射和动态代理

前三篇详细总结了Mybatis的基本特性、常用配置、映射器,相对于Hibernate,映射器的配置相对复杂,但有很好的灵活性和扩展性,可以应对各种业务场景。熟练...

4547
来自专栏架构师小秘圈

shell极简教程(二)

一,题记 不懂shell的程序员不是好程序员,学习shell是为了自动化,使用自动化可以非常有效的提高工作效率。没有一个大公司不要求linux的基本技能的,只是...

3967
来自专栏yukong的小专栏

【java并发编程实战3】解密volatilevolatile的使用场景

根据 as if serial原则,它强调了单线程。那么多线程发生重排序又是怎么样的呢?

782
来自专栏PHP实战技术

PHP垃圾回收机制

PHP垃圾回收机制 1、每一个变量定义时都保存在一个叫zval的容器里面,这里面包含了数量的类型和和值,还包含了一个refcount(理解为存在几个变量个数)和...

3274
来自专栏aCloudDeveloper

string 之 strrev函数

Author: bakari  Date: 2012/8/9 继上篇。。。。。 下面是我写的代码与源码作的一些比较,均已严格测试通过,分别以“string 之”...

1939

扫码关注云+社区

领取腾讯云代金券