对象大小的计算
正如笔者看的这篇文章所描述的一样,当我们试图获取一个JVM中的对象实际占用的空间大小时。通常使用的方法是徒手计算,或者通过gc计算前后内存差来估计对象大小。而今天要介绍的方法——利用Instrumentation
来准确获取对象大小的方法——拒绝手算,拒绝估算!
搭建HelloWorld
关键在于获取Instrumentation
的实例,通过查看此接口的JavaDoc可以知道有两种方法:
- *When a JVM is launched in a way that indicates an agent class. In that case an Instrumentation instance is passed to the premain method of the agent class. * 也就是说在JVM启动的时候会有一个
agent
类的premain
方法会接收到一个Instrumentation
实例。这种方法需要我们自己实现一个agent
的实现类,并复写premain方法
来保存此实例即可。 - When a JVM provides a mechanism to start agents sometime after the JVM is launched. In that case an Instrumentation instance is passed to the agentmain method of the agent code. 这种方法其实也是通过
agent
来获取实例,只不过这里的agent
是在JVM启动后才运行起来的。
我们这里通过第一种方法来做一个Demo,简单概括以下流程:
1. 编写agent
实现类,在类中覆写premain()
,并打成jar包(在这里就是AgentClass-jar-with-dependencies.jar)。注意在打包的时候需要修改META-INF/MANIFEST.MF
文件增加一行
Premain-Class: MyAgent
在这里我们是通过maven的maven-assembly-plugin
插件来实现的修改的。更多关于META-INF/MANIFEST.MF
可以看这篇文章。
- 编写调用
Instrumentation
实例的测试类 - 通过
-javaagent:jar/AgentClass-jar-with-dependencies.jar
参数启动测试类 - 注意:因为在jdk6之后默认开启了指针压缩,为了方便理解,最好在启动的时候加入
-XX:-UseCompressedOops
关闭之。
详细的代码结构:
.
├── jar
│ └── AgentClass-jar-with-dependencies.jar
├── pom.xml
└── src
└── main
└── java
├── MyAgent.java
└── TestSize.java
/*MyAgent.java*/
import java.lang.instrument.Instrumentation;
public class MyAgent {
private static Instrumentation inst;
/**
* 这个方法必须写,在agent调用时会被启用
*/
public static void premain(String agentArgs, Instrumentation instP) {
System.out.println("On premain, agentArgs: [" + agentArgs + "]");
inst = instP;
}
/**
* 用来测量java对象的大小,调用了Instrumentation实例方法
*/
public static long sizeOf(Object o) {
if(inst == 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 inst.getObjectSize(o);
}
}
/*pom.xml*/
<build>
<finalName>AgentClass</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<configuration>
<archive>
<manifestEntries>
<!-- 在这里添加Premain-Class: MyAgent到MENIFEST.MF -->
<Premain-Class>MyAgent</Premain-Class>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
/*TestSize.java*/
public class TestSize {
public void test(){
System.out.println("int:\t" + MyAgent.sizeOf(1));//输出为16
}
public static void main(String []args) {
new TestSize().test();
}
}
到这里我们就完成了一个简单的HelloWorld,如果想要深入了解Instrumentation
的细节,参考另一篇文章。下面我们继续来看我们得到的实验结果。
对象空间结构
普通对象结构
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。如下图所示:
对象头:包含两个部分,第一部分
markword
,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。第二部分是klass
,类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,同样在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit。对象实际数据:为实际在堆中分配了内存的类属性。
对齐填充:HotSpot VM的自动内存管理系统要求所有的对象起始位置必须是8的整数倍,所以对于不足
n*8byte
的对象进行填充对齐。
有了以上的信息,我们就可以进行更多的假设校验了,(以下数据来自64位jvm,关闭指针压缩)
类定义 | 大小(bytes) | 说明 |
---|---|---|
public class SimpleVainClass{} |
16 | 仅包含对象头 |
(int)1 |
24 | 猜测本来一个基础类型int仅包含4字节,但是因为转成了对象,加了一个对象头的大小16字节,补齐为24 |
Integer(1) |
24 | 同上 |
public class SimpleClassWithInt{ private int i = 3;} |
32 | 按理应该是对象头16字节 + int对象指针8字节 = 24字节,为什么这里出现32字节??望高人指教 |
参考链接:
1. 如何精确地测量java对象的大小——底层instrument API