对象内存结构
Class文件以字节码的形式存储在方法区当中,用来描述一个类本身的内存结构。当使用Class文件新建对象时,对象实例的内存结构又究竟是个什么样子呢?
如图所示,为了表示对象的属性、方法等信息,HotSpot VM使用对象头部的一个指针指向Class区域的方式来找到对象的Class描述,以及内部的方法、属性入口。除此之外,还在对象的头部划分了部分空间(Mark Word),用于描述与对象相关的其他信息,例如:是否加锁、GC标志位、Minor GC次数、对象默认的hashCode(System.identityHashCode(object)可获取对象的这个值)。
在32位系统下,存放Class指针的空间大小是4字节,Mark Word空间大小也是4字节,因此就是8字节的头部,如果是数组还需要增加4字节来表示数组的长度。
在64位系统及64位JVM下,开启指针压缩(参数是 -XX:+UseCompressedOops),那么头部存放Class指针的空间大小还是4字节,而Mark Word区域会变大,变成8字节,也就是头部最少为12字节。
若未开启指针压缩,那么保存Class指针的空间大小也会变成8字节,那么对象头部会变成16字节。另外,在64位模式下,若未开启压缩,引用也会变成8字节。
此外,Java对象将以8字节对齐在内存中,也就是对象占用的空间不是8字节的倍数,将会被补齐为8字节的倍数,这样做的好处是,在对象分配和查找的过程中不用考虑过多的偏移量问题。
以下是在32位系统下一些常见对象占用的空间大小示例。
没有继承的对象属性排布
在默认情况下,HotSpot VM会按照一个顺序排布对象的内部属性,这个顺序是,long/double-->int/float-->short/char-->byte/boolean-->Reference(与对象本身的属性顺序无关)。
有继承的对象属性排布
在HotSpot VM中,有继承关系的对象在创建时,父类的属性会被分配到相应的对象中,由于父类的属性不能和子类混用,所以它们必须单独排布在一个地方,可以认为它们就是从上到下的一个顺序。以两重继承为例,对象继承属性排布规则如下图所示。
这里的对齐有两种:一是整个对象的8字节对齐;二是父类到子类的属性对齐。在32位及64位压缩模式下,会按照4字节对齐。
例如下面的例子:
class A {byte b;}
class B extends A {byte b;}
class C extends B {byte b;}
如何计算对象大小
有时,我们需要知道Java对象到底占用多少内存,有人通过连续调用两次System.gc()比较两次gc前后内存的使用量在计算java对象的大小,也有人根据Java虚拟机规范中的Java对象内存排列估算对象的大小,这两种方法或多或少都有问题,因为System.gc()并不一定促发GC,同一个类型的对象在32位与64位JVM中使用的内存会不一样,在64位虚拟机中是否开启指针压缩也会影响Java对象在内存中的大小。
那么有没有一种既准确又方便的方法计算对象的大小呢?答案是肯定的。在Java 5中引入了Instrumentation类,这个类提供了计算对象内存占用量的方法;Hotspot支持instrumentation框架,其他的虚拟机也提供了类似的框架。
使用Instrumentation类计算Java对象大小的过程如下:
创建一个含有premain()方法的Java 类。
package sizeof;
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;
public class DeepObjectSizeOf {
private static Instrumentation inst;
public static void premain(String agentArgs, Instrumentation instP) {
inst = instP;
}
public static long sizeOf(Object object) {
//计算当前对象的内存大小,不包含引用对象
return inst.getObjectSize(object);
}
public static long deepSizeOf(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 needSkipObject(Object obj, Map<Object, Object> visited) {
if (obj instanceof 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 (needSkipObject(obj, visited)) {
return 0;
}
visited.put(obj, null);//将当前对象放入栈中
long result = 0;
result += sizeOf(obj);
Class <?>clazz = obj.getClass();
if (clazz.isArray()) {//如果数组
if(clazz.getName().length() != 2) {//如果primitive type array,Class的name为2位
int length = Array.getLength(obj);
for (int i = 0; i < length; i++) {
stack.add(Array.get(obj, i));
}
}
return result;
}
return getNodeSize(clazz , result , obj , stack);
}
//这个方法获取非数组对象自身的大小,并且可以向父类进行向上搜索
private static long getNodeSize(Class <?>clazz , long result , Object obj , Stack<Object> stack) {
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (!Modifier.isStatic(field.getModifiers())) {//这里抛开静态属性
if (field.getType().isPrimitive()) {//这里抛开基本关键字(因为基本关键字在调用java默认提供的方法就已经计算过了)
continue;
}else {
field.setAccessible(true);
try {
Object objectToAdd = field.get(obj);
if (objectToAdd != null) {
stack.add(objectToAdd);//将对象放入栈中,一遍弹出后继续检索
}
} catch (IllegalAccessException ex) {
assert false;
}
}
}
}
clazz = clazz.getSuperclass();//找父类class,直到没有父类
}
return result;
}
}
JVM会在应用程序运行之前调用这个Java 类的premain()方法(也就是在执行应用程序的main方法之前),JVM会在调用该方法时传入一个实现Instrumentation接口的实例,通过调用此接口实例的getObjectSize()方法可以计算出对象的大小(只计算当前对象的大小,不会进一步计算内部引用对象的大小)。
将创建好的Java类打成一个jar包
在打包之前先创建一个MANIFEST.txt文件作为这个jar包的清单文件,其内容如下:
Manifest-Version: 1.0
Premain-Class: sizeof.DeepObjectSizeOf
按照Java类文件的包路径创建好目录(DeepObjectSizeOf.class文件放在sizeof文件夹中)。
使用用如下命令创建jar包:
jar -cmf MANIFEST.txt java_sizeof.jar sizeof/*
修改JVM启动配置
修改Eclipse IDE的JVM启动配置,增加-javaagent启动参数:
-javaagent:jar文件路径
我创建的 java_sizeof.jar放在D:\sizeof目录下,设置参数如下。
测试样例
创建一个测试类SizeOfMain.java,代码如下。
package sizeof;
public class SizeOfMain {
public static void main(String[] args) {
System.out.println("new Integer(1) 对象大小:"
+ DeepObjectSizeOf.deepSizeOf(new Integer(1)));
System.out.println("new String(\"sizeof\") 对象大小:"
+ DeepObjectSizeOf.deepSizeOf(new String("sizeof")));
}
}
在64位机器上(不开启指针压缩):
设置参数:-javaagent:d:\sizeof/java_sizeof.jar -XX:-UseCompressedOops
执行结果:
在64位机器上(开启指针压缩):
设置参数:-javaagent:d:\sizeof/java_sizeof.jar -XX:+UseCompressedOops
执行结果:
---------------------
作者:pengjunlee
来源:CSDN
原文:https://blog.csdn.net/pengjunlee/article/details/72758619?utm_source=copy
版权声明:本文为博主原创文章,转载请附上博文链接!