最近在读《深入理解Java虚拟机》,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存?
在网上搜到了一篇博客讲的非常好:http://yueyemaitian.iteye.com/blog/2033046,里面提供的这个类也非常实用:
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
/**
* 对象占用字节大小工具类
*
*/
public class SizeOfObject {
static Instrumentation inst;
public static void premain(String args, Instrumentation instP) {
inst = instP;
}
/**
* 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、
* 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;
* 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小
*
* @param obj
* @return
*/
public static long sizeOf(Object obj) {
return inst.getObjectSize(obj);
}
/**
* 递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小
*
* @param objP
* @return
* @throws IllegalAccessException
*/
public static long fullSizeOf(Object objP) throws IllegalAccessException {
Set
大家可以用这个代码边看边验证,注意的是,运行这个程序需要通过javaagent注入Instrumentation,具体可以看原博客。我今天主要是总结下手动计算Java对象占用字节数的基本规则,做为基本的技能必须get√,希望能帮到和我一样的Java菜鸟。
在介绍之前,简单回顾下,Java对象的内存布局:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。另外:不同的环境结果可能有差异,我所在的环境是HotSpot虚拟机,64位Windwos。
下面进入正文:
对象头在32位系统上占用8bytes,64位系统上占用16bytes。
原生类型(primitive type)的内存占用如下:
Primitive Type | Memory Required(bytes) |
boolean | 在数组中占1个字节,单独使用时占4个字节 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
reference类型在32位系统上每个占用4bytes, 在64位系统上每个占用8bytes。
关于boolean内存占用 https://www.cnblogs.com/wangtianze/p/6690665.html?utm_source=itdadao&utm_medium=referral
HotSpot的对齐方式为8字节对齐:
(对象头 + 实例数据 + padding) % 8等于0且0 <= padding < 8
对象占用的内存大小收到VM参数UseCompressedOops的影响。32G内存以下的,默认开启对象指针压缩。
开启(-XX:+UseCompressedOops)对象头大小为12bytes(64位机器)。
static class A {
int a;
}
A对象占用内存情况:
关闭指针压缩: 16(对象头)+4(实例数据)=20不是8的倍数,因此需要对齐填充 16+4+4(padding)=24
开启指针压缩: 12+4=16已经是8的倍数了,不需要再padding。
64位机器上reference类型占用8个字节,开启指针压缩后占用4个字节。
static class B2 {
int b2a;
Integer b2b;
}
B2对象占用内存情况:
关闭指针压缩: 16+4+8=28不是8的倍数,需要对齐填充 16+4+8+4(padding)=32
开启指针压缩: 12+4+4=20不是8的倍数,需要对齐填充12+4+4+4(padding)=24
64位机器上,数组对象的对象头占用24个字节(8字节MarkWord+8字节类型指针+8字节数组长度),启用压缩之后占用16个字节(8字节MarkWord+4字节类型指针+4字节数组长度)。之所以比普通对象占用内存多是因为需要额外的对象头空间存储数组的长度。
先考虑下new Integer[0]占用的内存大小,数组长度为0,所以所占用的大小就是对象头的大小:
未开启压缩:24bytes
开启压缩后:16bytes
接着计算new Integer[1],new Integer[2],new Integer[3]和new Integer[4]就很容易了:
未开启压缩:
开启压缩:
拿new Integer[3]来具体解释下:
未开启压缩:24(对象头)+ 8*3 = 48,不需要padding;
开启压缩:16(对象头)+ 4*3 = 28,需要对齐填充 28 + 4(padding) = 32,其他依次类推。
自定义类的数组也是一样的,比如:
static class B3 {
int a;
Integer b;
}
new B3[3]占用的内存大小:
未开启压缩:24(对象头)+ 8*3 = 48
开启压缩后:16(对象头)+ 4*3 + 4(padding) = 32
计算复合对象占用内存的大小其实就是运用上面几条规则,只是麻烦点。
直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小; 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小。
static class B {
int a;
int b;
}
static class C {
int ba;
B[] as = new B[3];
C() {
for (int i = 0; i < as.length; i++) {
as[i] = new B();
}
}
}
计算C对象的大小:
未开启压缩:16(对象头)+ 4(ba)+ 8(as引用的大小)+ 4(padding) = 32
开启压缩:12(对象头)+ 4(ba)+4(as引用的大小)+ 4(padding) = 24
递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小。
递归计算复合对象占用的内存的时候需要注意的是:对齐填充是以每个对象为单位进行的,看下面这个图就很容易明白。
现在我们来手动计算下C对象占用的全部内存是多少,主要是三部分构成:C对象本身的大小+数组对象的大小+B对象的大小。
未开启压缩:
(16 + 4 + 8+4(padding)) + (24+ 8*3) +(16+4+4)*3 = 152bytes
开启压缩:
(12 + 4 + 4 +4(padding)) + (16 + 4*3 +4(数组对象padding)) + (12+4+4+4(B对象padding)) *3= 128bytes
涉及继承关系的时候有一个最基本的规则:首先存放父类中的成员,接着才是子类中的成员, 父类也要按照 8 byte 规定
public static class D {
byte d1;
}
public static class E extends D {
byte e1;
}
计算E对象的大小:
未开启压缩:16(对象头) + 父类(1(d1) + 7(padding)) + 1(e1) + 7(padding) = 32
开启压缩:12(对象头) + 父类(1(d1) + 7(padding)) + 1(e1) + 3(padding) = 24
大家有兴趣的可以试试。
实际工作中真正需要手动计算对象大小的场景应该很少,但是个人觉得做为基础知识每个Java开发人员都应该了解,另外:对自己写的代码大概占用多少内存,内存中是怎么布局的应该有一个直觉性的认识。