JVM —— Java 对象占用空间大小计算
零. 为什么要知道 Java 对象占用空间大小
- 缓存的实现: 在设计 JVM 内缓存时(不是借助 Memcached、 Redis 等), 须要知道缓存的对象是否会超过 JVM 最大堆限制, 假设会超过要设置对应算法如 LRU 来丢弃一部分缓存数据以满足兴许内容的缓存
- JVM 參数设置: 假设知道对象会被创建。 能够帮助推断 -Xmx 须要设置多少
- 仅仅是为了好玩
一. 对象的内存布局
HotSpot 虚拟机中。对象在内存中存储的布局能够分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
二. 对象头
JVM 对象头一般占用两个机器码,在 32-bit JVM 上占用 64bit, 在 64-bit JVM 上占用 128bit 即 8+8=16 bytes(开启指针压缩后占用 4+8=12 bytes)
64位机器上。数组对象的对象头占用 24 bytes,启用压缩之后占用 16 bytes。之所以比普通对象占用内存多是由于须要额外的空间存储数组的长度。
三. 实例数据
原生类型(primitive type)的内存占用例如以下:
Primitive Type | Memory Required(bytes) |
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
引用类型(reference type: Integer)在 32 位系统上每一个占用 4bytes(即32bit, 才干管理 2^32=4G 的内存), 在 64 位系统上每一个占用 8bytes(开启压缩为 4 bytes)。
四. 对齐填充
HotSpot 的对齐方式为 8 字节对齐。不足的须要 Padding 填充对齐, 公式:(对象头 + 实例数据 + padding)% 8 == 0 (0<= padding <8)
五. 计算 Java 对象占用空间大小
借助 Instrument 接口的 getObjectSize 方法计算对象占用空间
SizeOfAgent: 计算对象大小类
package com.wenniuwuren.objectsizeof; import java.lang.instrument.Instrumentation; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.IdentityHashMap; import java.util.Map; import java.util.Stack; /** * 借助 Instrumentation 接口的 getObjectSize 方法计算对象占用空间 * 原来的 sizeOf 仅仅能计算本对象占用空间, 无法计算继承下来的占用空间, * 只是能够用反射的方法把全部占用空间计算出来 * * Created by zhuyb on 16/3/20. */ public class SizeOfAgent { static Instrumentation instrumentation; // 第一个參数由 –javaagent。 第二个參数由 JVM 传入 public static void premain(String agentArgs, Instrumentation instP) { instrumentation = instP; } // 返回没有子类对象大小的大小 public static long sizeOf(Object o) { if (instrumentation == null) { throw new IllegalStateException("Can not access instrumentation environment.\n" + "Please check if jar file containing SizeOfAgent class is \n" + "specified in the java's \"-javaagent\" command line argument."); } return instrumentation.getObjectSize(o); } /** * * 计算复合对象 * @param obj object to calculate size of * @return object size */ public static long fullSizeOf(Object obj) { Map<Object, Object> visited = new IdentityHashMap<Object, Object>(); Stack<Object> stack = new Stack<Object>(); long result = internalSizeOf(obj, stack, visited); while (!stack.isEmpty()) { result += internalSizeOf(stack.pop(), stack, visited); } visited.clear(); return result; } // 这个算法使每一个对象仅被计算一次。 避免循环引用,即死循环计算 private static boolean skipObject(Object obj, Map<Object, Object> visited) { if (obj instanceof String) { // String 池里已有的不再计算 if (obj == ((String) obj).intern()) { return true; } } return (obj == null) // 已有对象不再计算 || visited.containsKey(obj); } private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) { if (skipObject(obj, visited)){ return 0; } visited.put(obj, null); long result = 0; // get size of object + primitive variables + member pointers result += SizeOfAgent.sizeOf(obj); // 处理全部数组内容 Class clazz = obj.getClass(); if (clazz.isArray()) { // [I , [F 基本类型名字长度是2 if(clazz.getName().length() != 2) {// skip primitive type array int length = Array.getLength(obj); for (int i = 0; i < length; i++) { stack.add(Array.get(obj, i)); } } return result; } // 处理对象的全部字段 while (clazz != null) { Field[] fields = clazz.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { // 不反复计算静态类型字段 if (!Modifier.isStatic(fields[i].getModifiers())) { // 不反复计算原始类型字段 if (fields[i].getType().isPrimitive()) { continue; } else { // 使 private 属性可訪问 fields[i].setAccessible(true); try { // objects to be estimated are put to stack Object objectToAdd = fields[i].get(obj); if (objectToAdd != null) { stack.add(objectToAdd); } } catch (IllegalAccessException ex) { assert false; } } } } clazz = clazz.getSuperclass(); } return result; } }
使用上述代码必须将上述代码打成 jar 包, 而且 MANIFEST.MF 文件设置參数(
Premain-Class: sizeof.agent.SizeOfAgent
Boot-Class-Path:
Can-Redefine-Classes: false
Can-Redefine-Classes: false
)
假设使用 Maven 打包的话, 能够直接在 pom.xml 里面设置 MANIFEST.MF 的參数 :
<!-- pom打jar包的时候设置MANIFEST.MF的key/value,能够通过在pom.xml文件里加入plugin的方式来实现 --> <build> <plugins> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>2.4</version> <configuration> <finalName>SizeOfAgent</finalName> <archive> <manifestEntries> <Premain-class>com.wenniuwuren.objectsizeof.SizeOfAgent</Premain-class> <Boot-Class-Path></Boot-Class-Path> <Can-Redefine-Classes>false</Can-Redefine-Classes> </manifestEntries> <addMavenDescriptor>false</addMavenDescriptor> </archive> </configuration> </plugin> </plugins> </build>
測试类: SizeOfAgentTest
package com.wenniuwuren.objectsizeof; import static com.wenniuwuren.objectsizeof.SizeOfAgent.*; /** * 下面结果在 64-bit JVM 下測试 * 启动參数1(不压缩指针长度):-javaagent:target/SizeOfAgent.jar -XX:-UseCompressedOops * * Created by zhuyb on 16/3/20. */ public class SizeOfAgentTest { public static void main(String[] args) { System.out.println("------------------空对象----------------------------"); // 16 bytes + 0 + 0 = 16 空对象, 仅仅有对象头 System.out.println("sizeOf(new Object()) = " + sizeOf(new Object())); System.out.println("fullSizeOf(new Object()) = " + fullSizeOf(new Object())); System.out.println("----------------非空对象含有原始类型、引用类型------------------------------"); // 16 bytes + 8 + 4 + padding = 32 System.out.println("sizeOf(new A()) = " + sizeOf(new A())); System.out.println("fullSizeOf(new A()) = " + fullSizeOf(new A())); // 16 + 4 + padding =24 数据是一个 int System.out.println("sizeOf(new Integer(1)) = " + sizeOf(new Integer(1))); // (16 + int hash:4 + int hash32:4 + refer char value[]:8 + padding) = 32 // 静态属性(static)不计算空间。由于全部对象都是共享一块空间的 // 不同版本号JDK可能 String 内部 Field 可能不同,本次測试使用JDK1.7 System.out.println("sizeOf(new String()) = " + sizeOf(new String())); // (16 + 4 + 4 + 8 + padding) + (24 + 0 + padding) = 56 System.out.println("fullSizeOf(new String()) = " + fullSizeOf(new String())); // (16 + 4 + 4 + 8 + padding) = 32 System.out.println("sizeOf(new String('a')) = " + sizeOf(new String("a"))); // (16 + 4 + 4 + 8 +padding) + (24 + 2 + padding) = 64 System.out.println("fullSizeOf(new String('a')) = " + fullSizeOf(new String("a"))); System.out.println("-------------------原始类型数组对象---------------------------"); // 24 bytes + 0*1 + 0 = 24 数组长度为 0,所以仅仅有对象头的长度 System.out.println("sizeOf(new byte[0]) = " + sizeOf(new byte[0])); System.out.println("fullSizeOf(new byte[0]) = " + fullSizeOf(new byte[0])); // 24 + 1*1 + padding = 32 System.out.println("sizeOf(new byte[1]) = " + sizeOf(new byte[1])); System.out.println("fullSizeOf(new byte[1]) = " + fullSizeOf(new byte[1])); // 24 + 1*2 + padding = 32 System.out.println("sizeOf(new char[1]) = " + sizeOf(new char[1])); System.out.println("fullSizeOf(new char[1]) = " + fullSizeOf(new char[1])); // 24 + 9*1 + padding = 40 System.out.println("sizeOf(new byte[9]) = " + sizeOf(new byte[9])); System.out.println("fullSizeOf(new byte[9]) = " + fullSizeOf(new byte[9])); System.out.println("--------------------引用类型数组对象--------------------------"); // 24 bytes + 0*8 + 0 = 24 数组长度为 0 System.out.println("sizeOf(new Integer[0]) = " + sizeOf(new Integer[0])); System.out.println("fullSizeOf(new Integer[0]) = " + fullSizeOf(new Integer[0])); // 24 bytes + 1*8 + 0 = 32 引用对象 64-bit JVM 占用 8 bytes System.out.println("sizeOf(new Integer[1]) = " + sizeOf(new Integer[1])); System.out.println("fullSizeOf(new Integer[1]) = " + fullSizeOf(new Integer[1])); // 24 bytes + 2*8 + padding = 40 System.out.println("sizeOf(new Integer[1]) = " + sizeOf(new Integer[1])); System.out.println("fullSizeOf(new Integer[1]) = " + fullSizeOf(new Integer[1])); // 24 + 3*8 + padding = 48 System.out.println("sizeOf(new Integer[3]) = " + sizeOf(new Integer[3])); System.out.println("fullSizeOf(new Integer[3]) = " + fullSizeOf(new Integer[3])); System.out.println("-------------------自己定义数组对象---------------------------"); // 16 + (4+8) + padding = 32 System.out.println("sizeOf(new B()) = " + sizeOf(new B())); System.out.println("fullSizeOf(new B()) = " + fullSizeOf(new B())); // 24 + 0*8 + padding = 24 引用对象 64-bit JVM 占用 8 bytes, // 由于没创建真实的 new B()所以 B类内部数据还未占用空间 System.out.println("sizeOf(new B[0]) = " + sizeOf(new B[0])); System.out.println("fullSizeOf(new B[0]) = " + fullSizeOf(new B[0])); // 24 + 1*8 + padding = 32 System.out.println("sizeOf(new B[1]) = " + sizeOf(new B[1])); System.out.println("fullSizeOf(new B[1]) = " + fullSizeOf(new B[1])); // 24 + 2*8 + padding = 40 System.out.println("sizeOf(new B[2]) = " + sizeOf(new B[2])); System.out.println("fullSizeOf(new B[2]) = " + fullSizeOf(new B[2])); // 24 + 3*8 + padding = 48 System.out.println("sizeOf(new B[3]) = " + sizeOf(new B[3])); System.out.println("fullSizeOf(new B[3]) = " + fullSizeOf(new B[3])); System.out.println("-------------------复合对象---------------------------"); // 16 + (4+8) + padding = 32 sizeOf 仅仅计算单层次占用空间大小 System.out.println("sizeOf(new C()) = " + sizeOf(new C())); // (16 + (4+8) + padding1) + (24 + 2*8 + padding2) + 2*(16 + (4+8) + padding3) = 136 // 递归计算当前对象占用空间总大小,包含当前类和超类的实例字段大小以及实例字段引用对象大小 System.out.println("fullSizeOf(new C()) = " + fullSizeOf(new C())); System.out.println("-------------------继承关系---------------------------"); // 涉及继承关系的时候有一个最主要的规则:首先存放父类中的成员,接着才是子类中的成员, 父类也要依照 8 byte 规定 // 16 + 1 + padding = 24 System.out.println("sizeOf(new D()) = " + sizeOf(new D())); System.out.println("fullSizeOf(new D()) = " + fullSizeOf(new D())); // 16 + 父类(1 + padding1) + 1 + padding2 = 32 System.out.println("sizeOf(new E()) = " + sizeOf(new E())); System.out.println("fullSizeOf(new E()) = " + fullSizeOf(new E())); } public static class A { int a; Integer b; } public static class B { int a; Integer b; } public static class C{ int c; B[] b = new B[2]; // 初始化 C() { for (int i = 0; i < b.length; i++) { b[i] = new B(); } } } public static class D { byte d1; } public static class E extends D { byte e1; } }
执行:
假设在 IDE 执行时须要设置 JVM 參数: -javaagent:target/SizeOfAgent.jar -XX:-UseCompressedOops;
假设在命令行执行命令: java -javaagent:sizeofag.jar
-XX:-UseCompressedOops 主类名称。
測试结果:
------------------空对象---------------------------- sizeOf(new Object()) = 16 fullSizeOf(new Object()) = 16 ----------------非空对象含有原始类型、引用类型------------------------------ sizeOf(new A()) = 32 fullSizeOf(new A()) = 32 sizeOf(new Integer(1)) = 24 sizeOf(new String()) = 32 fullSizeOf(new String()) = 56 sizeOf(new String('a')) = 32 fullSizeOf(new String('a')) = 64 -------------------原始类型数组对象--------------------------- sizeOf(new byte[0]) = 24 fullSizeOf(new byte[0]) = 24 sizeOf(new byte[1]) = 32 fullSizeOf(new byte[1]) = 32 sizeOf(new char[1]) = 32 fullSizeOf(new char[1]) = 32 sizeOf(new byte[9]) = 40 fullSizeOf(new byte[9]) = 40 --------------------引用类型数组对象-------------------------- sizeOf(new Integer[0]) = 24 fullSizeOf(new Integer[0]) = 24 sizeOf(new Integer[1]) = 32 fullSizeOf(new Integer[1]) = 32 sizeOf(new Integer[1]) = 32 fullSizeOf(new Integer[1]) = 32 sizeOf(new Integer[3]) = 48 fullSizeOf(new Integer[3]) = 48 -------------------自己定义数组对象--------------------------- sizeOf(new B()) = 32 fullSizeOf(new B()) = 32 sizeOf(new B[0]) = 24 fullSizeOf(new B[0]) = 24 sizeOf(new B[1]) = 32 fullSizeOf(new B[1]) = 32 sizeOf(new B[2]) = 40 fullSizeOf(new B[2]) = 40 sizeOf(new B[3]) = 48 fullSizeOf(new B[3]) = 48 -------------------复合对象--------------------------- sizeOf(new C()) = 48 fullSizeOf(new C()) = 152 -------------------继承关系--------------------------- sizeOf(new D()) = 24 fullSizeOf(new D()) = 24 sizeOf(new E()) = 32 fullSizeOf(new E()) = 32
測试类中复合对象计算可能较为麻烦, 能够參照下图较为清楚地看出 new C() 的占用空间计算:
六. 总结
总体的 Java 对象是依照一定规则进行的。 清楚了 JVM 对象的内存布局和分配规则。 计算 Java 对象的大小就比較简单了。
Java 不像 C++ 能够提供对象大小, 这是 Java 语言的设计初衷(自己主动内存管理), 可是随着对 Java 的深入了解。 又到了对 JVM (使用 C、C++ 实现) 底层实现的问题上。
本文的參考资料为 2007 年的, 至今已有 9 年, 參考资料内容至今还是有效的,JVM 相关的东西变动确实小,挺有意思的
七. 參考资料