在进行 JVM 调优时,我们经常关注 JVM 各个区域大小以及相关参数,从而进行特定的优化,在一次排查内存溢出问题时我不禁想到一个问题,一个 Java 对象到底占用多大内存?下面我们就来分析验证下。
Java 对象内存结构
在 JVM 中,Java 对象都是在堆内存上分配的,想要分析出 Java 对象内存占用,首先要了解 Java 对象内存结构,一个 Java 对象内存占用由三部分组成:对象头(Header)
,实例数据(Instance Data)
和对齐填充(Padding)
。
对象头(Header)
对象头的组成
虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据
,如 hashCode
、GC分代年龄
、锁状态标志
、线程持有的锁
、偏向线程ID
、偏向时间戳
等。这部分数据的长度在 32 位和 64 位的虚拟机(未开启指针压缩)中分别为 4B 和 8B ,官方称之为 ”Mark Word”
。
对象的另一部分是类型指针(kclass)
,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。另外如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中却无法确定数组的大小。同样,这部分数据的长度在 32 位和 64 的虚拟机(未开启指针压缩)中分别为 4B 和 8B。
指针压缩
从 JDK 1.6 update14 开始,64 bit JVM 正式支持了 -XX:+UseCompressedOops
这个可以压缩指针,起到节约内存占用的新参数。
如果 UseCompressedOops
是打开的,则以下对象的指针会被压缩:
所有对象的 klass 属性
所有对象指针实例的属性
所有对象指针数组的元素(objArray)
由此我们可以计算出对象头大小:
32位虚拟机对象头大小= Mark Word(4B)+ kclass(4B) = 8B
64位虚拟机对象头大小= Mark Word(8B)+ kclass(4B) = 12B
实例数据
一个 Java 对象中的实例数据可能包括两种,一是 8 种基本类型,二是实例数据也是一个对象,说到这里很多人可能有个误区:
基本类型?基本类型不是在栈上分配内存的吗?怎么要计算到分配在堆内存上对象的大小里面去?
基本类型在栈上分配内存?其实并不是,所谓“栈内存保存基本类型以及对象的引用(reference),堆内存保存对象” 只是一句不严谨的话,实际仔细研究起来,栈内存(更专业的术语叫做堆栈)作为虚拟机作为方法调用和方法执行的数据结构,可能保存五种信息:
局部变量表
操作数栈
动态链接
方法返回地址
附加信息
其中局部变量表中存储了方法中的局部变量,可能为 8 种基本类型或者 reference
也就是说,栈内存中保存的基本类型,都是方法中的局部变量,而如果基本类型作为对象的实例变量,是在堆上分配空间的,此外,如果实例变量被final修饰,则既不在栈也不在堆上分配空间,而是分配到常量池里面。
8 种基本类型和 reference 大小在虚拟机上都是固定的,见下表
Primitive Type | Memory Required(bytes) |
---|---|
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
Reference | 4 |
对齐填充(Padding)
由于虚拟机内存管理体系要求 Java 对象内存起始地址必须为 8 的整数倍,换句话说,Java 对象大小必须为 8 的整数倍,当对象头+实例数据大小不为 8 的整数倍时,将会使用Padding机制进行填充,譬如, 64 位虚拟机上 new Object() 实际大小为:
Mark Word(8B)+ kclass(4B)[开启指针压缩] = 12B
但由于Padding机制,实际占用空间为: Mark Word(8B)+ kclass(4B)[开启指针压缩]+Padding(4B) = 16B
数组的大小
Java 中数组也是一种对象,数组的大小与普通 Java 对象相比多了数组长度的信息(4B),即一个数组对象大小为 Mark Word(8B)+ kclass(4B)[开启指针压缩] + 数组长度(4B) = 16B
使用Instrumentation计算 Java 对象大小
现在我们已经知道了一个 Java 对象的大小 = 对象头 + 实例数据 + Padding ,现在,我们验证一下计算结果,google 到一个 Instrumentation
刚好可以计算对象大小
Instrumentation
是 Java SE 5 引入的特性,使用 Instrumentation
,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够实现字节码修改技术。简单的说,Instrumentation
实现了一个虚拟机层面的 AOP 。
本文不涉及 Instrumentation
的复杂应用,我们只使用 Instrumentation
其中一个 getObjectSize()
方法获取对象大小。
使用 Instrumentation
需要使用 javaagent 技术,
简单说就是运行一个带 main 函数的类时可以通过 –javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation
代理)来启动 Instrumentation
的代理程序。具体分为三步:
一 编写一个Instrumentation类作为代理
其中 premain 注入 Instrumentation ,sizeOf 用来计算对象占用空间
ObjectShallowSize.java:
package sizeof;
import java.lang.instrument.Instrumentation;
public class ObjectShallowSize {
private static Instrumentation inst;
public static void premain(String agentArgs, Instrumentation instP){
inst = instP;
}
public static long sizeOf(Object obj){
return inst.getObjectSize(obj);
}
}
二 打包
在 ObjectShallowSize.java 路径下新建 /META-INF/MANIFEST.MF 指定 Premain-Class 内容为:
Manifest-Version: 1.0
Premain-Class: sizeof.ObjectShallowSize
然后编译,打包
javac -d . ObjectShallowSize.java
jar cvfm java-agent-sizeof.jar META-INF/MANIFEST.MF .
三 运行
编写一个测试模版类 ObjectSizeTest.java ,使用
java -javaagent:java-agent-sizeof.jar ObjectSizeTest
来运行程序
ObjectSizeTest.java 代码如下:
package sizeof;
public class ObjectSizeTest {
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest));
}
}
ObjectSizeTest 没有实例变量,理论计算
ObjectSizeTest大小 = Mark Word(8B)+ kclass(4B) [开启指针压缩]+Padding(4B) = 16B
为了方便,我们在 IDEA 中验证一下,导入刚才的 ObjectSizeTest 类,指定 JVM 参数如图
运行结果为 16B,和我们猜想一致
接下来我们在模版类中添加几个实例变量验证下
一
package sizeof;
public class ObjectSizeTest {
private int i;
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
}
}
理论值:Mark Word(8B)+ kclass(4B) + i(4B) = 16B
实际值:16B
二
package sizeof;
public class ObjectSizeTest {
private int i;
private int j;
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
}
}
理论值:Mark Word(8B)+ kclass(4B) + i(4B) + j(4B)+Padding(4B) = 24B
实际值:24B
三
package sizeof;
public class ObjectSizeTest {
private int i;
private int j;
private String s;
private boolean aBoolean;
private char c;
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
}
}
理论值:Mark Word(8B)+ kclass(4B) + i(4B) + j(4B) + s(4B) + aBoolean(1B) + c(2B) + Paddding(5B) = 32B
实际值:32B
四
package sizeof;
public class ObjectSizeTest {
private String s; // 4
private int i1; // 4
private byte b1; // 1
private byte b2; // 1
private int i2;// 4
private Object obj; //4
private byte b3; // 1
public static void main(String[] args) {
System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
}
}
理论值:Mark Word(8B)+ kclass(4B) + s(4B) + i1(4B) + b1(1B) + b2(1B) + 2(padding) + i2(4B) + obj(4B)+ b3(1B) + Paddding(7B) = 40B
实际值:32B
纳尼?这里为什么理论值和实际值不一致?
事实上,HotSpot创建的对象的字段会先按照给定顺序排列一下,默认的顺序如下,从长到短排列,引用排最后: long/double --> int/float --> short/char --> byte/boolean --> Reference
这个顺序可以使用JVM参数: -XX:FieldsAllocationSylte = 0
(默认是1)来改变。
按照这种方法我们来重新计算下对象大小
Mark Word(8B)+ kclass(4B) + i1(4B) + i2(4B) + b1(1B) + b2(1B) + b3(1B) + Paddding(1B) + s(4B) + obj(4B) = 32B
与预期值一致。
Java对象实际大小
前面我们计算 Java 对象大小时,对于实例变量为对象的,只计算了其reference的大小,实际应该也将实例变量本身计算在内,我们可以通过反射机制取出 Java 对象中实例变量,递归计算累加出实际大小。 http://yueyemaitian.iteye.com/blog/2033046 已经提供了现成的程序如下,使用fullSizeOf()方法即可计算出 Java 对象实际大小。
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;
/**
* 对象占用字节大小工具类
* <p>
*
* @author tianmai.fh
* @date 2014-03-18 11:29
*/
public class SizeOfObject {
static Instrumentation inst;
public static void premain(String args, Instrumentation instP) {
inst = instP;
}
/**
* 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、<br></br>
* 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;<br></br>
* 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小 <br></br>
*
* @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<Object> visited = new HashSet<Object>();
Deque<Object> toBeQueue = new ArrayDeque<Object>();
toBeQueue.add(objP);
long size = 0L;
while (toBeQueue.size() > 0) {
Object obj = toBeQueue.poll();
//sizeOf的时候已经计基本类型和引用的长度,包括数组
size += skipObject(visited, obj) ? 0L : sizeOf(obj);
Class<?> tmpObjClass = obj.getClass();
if (tmpObjClass.isArray()) {
//[I , [F 基本类型名字长度是2
if (tmpObjClass.getName().length() > 2) {
for (int i = 0, len = Array.getLength(obj); i < len; i++) {
Object tmp = Array.get(obj, i);
if (tmp != null) {
//非基本类型需要深度遍历其对象
toBeQueue.add(Array.get(obj, i));
}
}
}
} else {
while (tmpObjClass != null) {
Field[] fields = tmpObjClass.getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers()) //静态不计
|| field.getType().isPrimitive()) { //基本类型不重复计
continue;
}
field.setAccessible(true);
Object fieldValue = field.get(obj);
if (fieldValue == null) {
continue;
}
toBeQueue.add(fieldValue);
}
tmpObjClass = tmpObjClass.getSuperclass();
}
}
}
return size;
}
/**
* String.intern的对象不计;计算过的不计,也避免死循环
*
* @param visited
* @param obj
* @return
*/
static boolean skipObject(Set<Object> visited, Object obj) {
if (obj instanceof String && obj == ((String) obj).intern()) {
return true;
}
return visited.contains(obj);
}
}
参考资料:
《深入理解Java虚拟机》
感谢阅读,原创不易,如有启发,点个赞吧!这将是我写作的最强动力!本文不同步发布于不止于技术的技术公众号
Nauyus
,主要分享一些编程语言,架构设计,思维认知类文章, 2019年12月起开启周更模式,欢迎关注,共同学习成长!