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