Java虚拟机--你的对象有多大

作者: 贾博岩 | 来源:发表于2018-05-04 22:55 被阅读602次

    如何计算对象大小

    上文中,笔者提到了对象头,并且说到了对象头中的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,即遇到引用时,只计算引用的长度,不计算所引用对象的实际大小。如果要计算所引用对象的实际大小,可以通过递归的方式去计算。本文暂不介绍此方式,有兴趣的朋友可以去网上查阅相关资料。

    相关文章

      网友评论

        本文标题:Java虚拟机--你的对象有多大

        本文链接:https://www.haomeiwen.com/subject/lrwjrftx.html