Java编程中,了解对象的内存开销非常重要。本文主要分析Java语言中对象的内存占用。
1、Java对象内存占用简介
java对象在内存中占用的空间分为3类:
- 对象头(Header);
- 实例数据(Instance Data);
- 对齐填充(Padding)。
我们常说的基础数据类型大小,如byte占1字节、int占4字节、long占8字节等,主要是指第二类实例数据。下图是Java对象内存占用的示意图。
1.1 对象头
HotSpot虚拟机的对象头包括两部分信息:markword和klass 。
1.1.1 对象头组成介绍
第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度,也就是一个int类型的对象,占4字节。
1.1.2 对象头占用空间
在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
在64位开启指针压缩的情况下-XX:+UseCompressedOops
,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。
如果对象是数组,那么额外增加4个字节,这块儿用来记录数组的长度。
1.2 实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
这部分空间的占用,就等于对象各个成员变量的空间占用加和。
1.3 对齐填充
对齐填充空间并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。这是由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。
很多其它编程语言在内存管理时,也有对齐填充的概念。
2、对象内存占用的例子
// 对象A: 对象头12B + 内部对象s引用 4B + 内部对象i 基础类型int 4B + 对齐 4B = 24B
// 内部对象s 对象头12B + 2个内部的int类型8B + 内部的char[]引用 4B + 对齐0B = 24B
// 内部对象str的内部对象char数组 对象头12B + 数组长度4B + 对齐0B = 16B
// 总: 对象A 24+ 内部对象s 24B + 内部对象s的内部对象char数组 16B =64B
class A {
String s = new String();
int i = 0;
}
// 对象B:对象头12B + 内部对象s引用 4B + 内部对象i 基础类型int 4B + 对齐 4B = 24B
// s没有被分配堆内存空间
// 总: 对象B 24B
class B {
String s;
int i = 0;
}
3、如何用程序验证Java对象在运行时的内存占用
上面介绍了Java对象在内存中存储的空间消耗情况,接下来要考虑能否在Java程序中统计某个对象的实际内存占用。主要有下面几种方法。
3.1 通过监测JVM内存的使用情况
这种方法是最简单的方法,也就是在对象创建之前统计下当前JVM内存的使用情况,再创建某个对象之后,再统计一遍,这两者的差值就是创建该对象使用的内存消耗。
这种方法,可能不太准确,因为粒度总体来说比较粗,除了新建对象的占用之外,JVM可能也有其他内存开销。所以,这块儿只适用于粗略的统计。
下面是一个示例代码,统计1到50000作为字符串用+
拼接时的内存消耗:
import java.io.IOException;
import java.util.Date;
public class Test {
public static void main(String args[]) throws IOException{
Runtime run = Runtime.getRuntime();
run.gc();//让JVM执行一次内存回收,这样统计会更准确
Date startTime = new Date();
System.out.println("time: " + startTime);
//获取开始时内存使用量
long startMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + startMem + "M" );
String str = "";
for(int i=0; i<50000; ++i){
str += I;
}
Date endTime = new Date();
System.out.println("time: " + endTime);
//获取结束时内存使用量
long endMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + endMem + "M" );
//统计内存开销和时间开销
System.out.println("memory difference:" + (endMem-startMem) + "M");
System.out.println("Time difference:" + (endTime.getTime()-startTime.getTime()) + "ms");
}
}
运行结果如下:
time: Sat Mar 23 23:06:24 CST 2019
memory> total:123M free:120M used:3M
time: Sat Mar 23 23:06:32 CST 2019
memory> total:545M free:435M used:110M
memory difference:107M
Time difference:7181ms
Process finished with exit code 0
从结果上可以看出,+
连接字符串的内存开销和内存确实超乎想象。下面再试下通过StringBuffer和StringBuffer做字符串拼接时的情况:
(1) StringBuilder拼接字符串的时空开销统计
import java.io.IOException;
import java.util.Date;
public class Test {
public static void main(String args[]) throws IOException{
Runtime run = Runtime.getRuntime();
run.gc();//让JVM执行一次内存回收,这样统计会更准确
Date startTime = new Date();
System.out.println("time: " + startTime);
//获取开始时内存使用量
long startMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + startMem + "M" );
StringBuilder sb = new StringBuilder();
for(int i=0; i<50000; ++i){
sb.append(String.valueOf(i));
}
String str = sb.toString();
Date endTime = new Date();
System.out.println("time: " + endTime);
//获取结束时内存使用量
long endMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + endMem + "M" );
//统计内存开销和时间开销
System.out.println("memory difference:" + (endMem-startMem) + "M");
System.out.println("Time difference:" + (endTime.getTime()-startTime.getTime()) + "ms");
}
}
运行结果:
time: Sat Mar 23 23:14:30 CST 2019
memory> total:123M free:120M used:3M
time: Sat Mar 23 23:14:30 CST 2019
memory> total:123M free:116M used:7M
memory difference:4M
Time difference:44ms
Process finished with exit code 0
(2) StringBuffer拼接字符串的时空开销统计
import java.io.IOException;
import java.util.Date;
public class Test {
public static void main(String args[]) throws IOException{
Runtime run = Runtime.getRuntime();
run.gc();//让JVM执行一次内存回收,这样统计会更准确
Date startTime = new Date();
System.out.println("time: " + startTime);
//获取开始时内存使用量
long startMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + startMem + "M" );
StringBuffer sb = new StringBuffer();
for(int i=0; i<50000; ++i){
sb.append(String.valueOf(i));
}
String str = sb.toString();
Date endTime = new Date();
System.out.println("time: " + endTime);
//获取结束时内存使用量
long endMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + endMem + "M" );
//统计内存开销和时间开销
System.out.println("memory difference:" + (endMem-startMem) + "M");
System.out.println("Time difference:" + (endTime.getTime()-startTime.getTime()) + "ms");
}
}
运行结果:
time: Sat Mar 23 23:11:45 CST 2019
memory> total:123M free:119M used:4M
time: Sat Mar 23 23:11:45 CST 2019
memory> total:123M free:115M used:8M
memory difference:4M
Time difference:62ms
Process finished with exit code 0
从上面的例子可以看出,StringBuffer和StringBuilder在字符串拼接方面的表现要明显优于+
。所以,大家在编程时一定要注意,尽量避免使用+
拼接字符串。
3.2 使用Unsafe类
3.2.1 Unsafe类介绍
Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。
3.2.2 统计内存占用的方法
这里我们使用Unsafe类,主要是用到这个类的objectFieldOffset()
方法。这个方法可以返回某个类对象的每一个field在内存中的offset(这个偏移是相对于类对象所占内存区域起始位置的偏移)。
这样,我们遍历成员变量的offset,然后,将它们从小到大排序,这样就得到了对象在内存中位置最靠后的field的offset,加上这个field所占的空间大小,然后在加上对齐填充(Padding,这部分不一定有),就得到了Java对象在内存中的占用空间大小。不过,这个大小可以说是对象内存占用浅空间大小(Shallow Size),因为有的field可能是一个对象引用,在对象的内存区域中只是存了一个指针。如果要获得一个Java对象的深度空间占用(Deep Size),就需要对这些指针进行递归遍历,将得到的空间求总和。
对于数组的处理稍微有些不同:
Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。
为了防止双向链表这样的结构在内存递归统计的过程中,出现循环递归,重复统计,这里需要记录下已经统计过内存占用的对象。下面是代码(这块儿代码来自连接:计算java对象的内存占用):
ObjectInfo.java:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* This class contains object info generated by ClassIntrospector tool
*/
public class ObjectInfo {
/** Field name */
public final String name;
/** Field type name */
public final String type;
/** Field data formatted as string */
public final String contents;
/** Field offset from the start of parent object */
public final int offset;
/** Memory occupied by this field */
public final int length;
/** Offset of the first cell in the array */
public final int arrayBase;
/** Size of a cell in the array */
public final int arrayElementSize;
/** Memory occupied by underlying array (shallow), if this is array type */
public final int arraySize;
/** This object fields */
public final List<ObjectInfo> children;
public ObjectInfo(String name, String type, String contents, int offset, int length, int arraySize,
int arrayBase, int arrayElementSize) {
this.name = name;
this.type = type;
this.contents = contents;
this.offset = offset;
this.length = length;
this.arraySize = arraySize;
this.arrayBase = arrayBase;
this.arrayElementSize = arrayElementSize;
children = new ArrayList<ObjectInfo>(1);
}
public void addChild(final ObjectInfo info) {
if (info != null) {
children.add(info);
}
}
/**
* Get the full amount of memory occupied by a given object. This value may be slightly less than
* an actual value because we don't worry about memory alignment - possible padding after the last object field.
*
* The result is equal to the last field offset + last field length + all array sizes + all child objects deep sizes
*
* @return Deep object size
*/
public long getDeepSize() {
//return length + arraySize + getUnderlyingSize( arraySize != 0 );
return addPaddingSize(arraySize + getUnderlyingSize(arraySize != 0));
}
long size = 0;
private long getUnderlyingSize(final boolean isArray) {
//long size = 0;
for (final ObjectInfo child : children)
size += child.arraySize + child.getUnderlyingSize(child.arraySize != 0);
if (!isArray && !children.isEmpty()) {
int tempSize = children.get(children.size() - 1).offset + children.get(children.size() - 1).length;
size += addPaddingSize(tempSize);
}
return size;
}
private static final class OffsetComparator implements Comparator<ObjectInfo> {
@Override
public int compare(final ObjectInfo o1, final ObjectInfo o2) {
return o1.offset - o2.offset; //safe because offsets are small non-negative numbers
}
}
//sort all children by their offset
public void sort() {
Collections.sort(children, new OffsetComparator());
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
toStringHelper(sb, 0);
return sb.toString();
}
private void toStringHelper(final StringBuilder sb, final int depth) {
depth(sb, depth).append("name=").append(name).append(", type=").append(type)
.append(", contents=").append(contents).append(", offset=").append(offset)
.append(", length=").append(length);
if (arraySize > 0) {
sb.append(", arrayBase=").append(arrayBase);
sb.append(", arrayElemSize=").append(arrayElementSize);
sb.append(", arraySize=").append(arraySize);
}
for (final ObjectInfo child : children) {
sb.append('\n');
child.toStringHelper(sb, depth + 1);
}
}
private StringBuilder depth(final StringBuilder sb, final int depth) {
for (int i = 0; i < depth; ++i)
sb.append("\t");
return sb;
}
private long addPaddingSize(long size) {
if (size % 8 != 0) {
return (size / 8 + 1) * 8;
}
return size;
}
}
ClassIntrospector.java
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import sun.misc.Unsafe;
/**
* This class could be used for any object contents/memory layout printing.
*/
public class ClassIntrospector {
private static final Unsafe unsafe;
/** Size of any Object reference */
private static final int objectRefSize;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
objectRefSize = unsafe.arrayIndexScale(Object[].class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** Sizes of all primitive values */
private static final Map<Class, Integer> primitiveSizes;
static {
primitiveSizes = new HashMap<Class, Integer>(10);
primitiveSizes.put(byte.class, 1);
primitiveSizes.put(char.class, 2);
primitiveSizes.put(int.class, 4);
primitiveSizes.put(long.class, 8);
primitiveSizes.put(float.class, 4);
primitiveSizes.put(double.class, 8);
primitiveSizes.put(boolean.class, 1);
}
/**
* Get object information for any Java object. Do not pass primitives to
* this method because they will boxed and the information you will get will
* be related to a boxed version of your value.
*
* @param obj
* Object to introspect
* @return Object info
* @throws IllegalAccessException
*/
public ObjectInfo introspect(final Object obj)
throws IllegalAccessException {
try {
return introspect(obj, null);
} finally { // clean visited cache before returning in order to make
// this object reusable
m_visited.clear();
}
}
// we need to keep track of already visited objects in order to support
// cycles in the object graphs
private IdentityHashMap<Object, Boolean> m_visited = new IdentityHashMap<Object, Boolean>(100); //这里的100是初始大小,超过会自增
private ObjectInfo introspect(final Object obj, final Field fld)
throws IllegalAccessException {
// use Field type only if the field contains null. In this case we will
// at least know what's expected to be
// stored in this field. Otherwise, if a field has interface type, we
// won't see what's really stored in it.
// Besides, we should be careful about primitives, because they are
// passed as boxed values in this method
// (first arg is object) - for them we should still rely on the field
// type.
boolean isPrimitive = fld != null && fld.getType().isPrimitive();
boolean isRecursive = false; // will be set to true if we have already
// seen this object
if (!isPrimitive) {
if (m_visited.containsKey(obj)) {
isRecursive = true;
}
m_visited.put(obj, true);
}
final Class type = (fld == null || (obj != null && !isPrimitive)) ? obj
.getClass() : fld.getType();
int arraySize = 0;
int baseOffset = 0;
int indexScale = 0;
if (type.isArray() && obj != null) {
baseOffset = unsafe.arrayBaseOffset(type);
indexScale = unsafe.arrayIndexScale(type);
arraySize = baseOffset + indexScale * Array.getLength(obj);
}
final ObjectInfo root;
if (fld == null) {
root = new ObjectInfo("", type.getCanonicalName(), getContents(obj,
type), 0, getShallowSize(type), arraySize, baseOffset,
indexScale);
} else {
final int offset = (int) unsafe.objectFieldOffset(fld);
root = new ObjectInfo(fld.getName(), type.getCanonicalName(),
getContents(obj, type), offset, getShallowSize(type),
arraySize, baseOffset, indexScale);
}
if (!isRecursive && obj != null) {
if (isObjectArray(type)) {
// introspect object arrays
final Object[] ar = (Object[]) obj;
for (final Object item : ar)
if (item != null) {
root.addChild(introspect(item, null));
}
} else {
for (final Field field : getAllFields(type)) {
if ((field.getModifiers() & Modifier.STATIC) != 0) {
continue;
}
field.setAccessible(true);
root.addChild(introspect(field.get(obj), field));
}
}
}
root.sort(); // sort by offset
return root;
}
// get all fields for this class, including all superclasses fields
private static List<Field> getAllFields(final Class type) {
if (type.isPrimitive()) {
return Collections.emptyList();
}
Class cur = type;
final List<Field> res = new ArrayList<Field>(10);
while (true) {
Collections.addAll(res, cur.getDeclaredFields());
if (cur == Object.class) {
break;
}
cur = cur.getSuperclass();
}
return res;
}
// check if it is an array of objects. I suspect there must be a more
// API-friendly way to make this check.
private static boolean isObjectArray(final Class type) {
if (!type.isArray()) {
return false;
}
if (type == byte[].class || type == boolean[].class
|| type == char[].class || type == short[].class
|| type == int[].class || type == long[].class
|| type == float[].class || type == double[].class) {
return false;
}
return true;
}
// advanced toString logic
private static String getContents(final Object val, final Class type) {
if (val == null) {
return "null";
}
if (type.isArray()) {
if (type == byte[].class) {
return Arrays.toString((byte[]) val);
} else if (type == boolean[].class) {
return Arrays.toString((boolean[]) val);
} else if (type == char[].class) {
return Arrays.toString((char[]) val);
} else if (type == short[].class) {
return Arrays.toString((short[]) val);
} else if (type == int[].class) {
return Arrays.toString((int[]) val);
} else if (type == long[].class) {
return Arrays.toString((long[]) val);
} else if (type == float[].class) {
return Arrays.toString((float[]) val);
} else if (type == double[].class) {
return Arrays.toString((double[]) val);
} else {
return Arrays.toString((Object[]) val);
}
}
return val.toString();
}
// obtain a shallow size of a field of given class (primitive or object
// reference size)
private static int getShallowSize(final Class type) {
if (type.isPrimitive()) {
final Integer res = primitiveSizes.get(type);
return res != null ? res : 0;
} else {
return objectRefSize;
}
}
}
TestClassIntrospector.java:
/**
* Created by chengxia on 2019/3/24.
*/
public class TestClassIntrospector {
private static class ObjectA {
String str; // 4
int i1; // 4
byte b1; // 1
byte b2; // 1
int i2; // 4
byte b3; // 1
ObjectB obj; //4
}
private static class ObjectB {
}
public static void main(String []args){
try {
final ClassIntrospector ci = new ClassIntrospector();
ObjectInfo res;
System.out.println("int:" + ci.introspect(new Integer(2)).getDeepSize());
System.out.println("str3:" + ci.introspect("abcd").getDeepSize());
res = ci.introspect(new ObjectA());
System.out.println("ObjectA:" + res.getDeepSize());
}catch (Exception e){
e.printStackTrace();
}
}
}
运行结果:
int:16
str3:48
ObjectA:32
Process finished with exit code 0
3.3 通过Instrumentation来统计
要通过这种方式来统计对象内存占用的大小,首先要了解java代理。
3.3.1 java代理
JavaAgent是JDK 1.5以后引入的。也叫Java代理。它是运行在main方法之前的拦截器,它内定的方法名叫 premain,也就是说先执行premain方法然后再执行main方法。
agent的代码与你的main方法在同一个JVM中运行,并被同一个system classloader装载,被同一的安全策略(security policy)和上下文(context)所管理。
实现一个JavaAgent只需要增加premain方法即可。如下代码(来自于链接:JavaAgent 简单例子):
import java.lang.instrument.Instrumentation;
public class MyAgent {
/**
* 该方法在main方法之前运行,与main方法运行在同一个JVM中
* 并被同一个System ClassLoader装载
* 被统一的安全策略(security policy)和上下文(context)管理
*
* @param agentOps
* @param inst
*/
public static void premain(String agentOps, Instrumentation inst) {
System.out.println("=========premain方法执行========");
System.out.println(agentOps);
}
/**
* 如果不存在 premain(String agentOps, Instrumentation inst)
* 则会执行 premain(String agentOps)
*
* @param agentOps
*/
public static void premain(String agentOps) {
System.out.println("=========premain方法执行2========");
System.out.println(agentOps);
}
}
但是,这个java代理运行起来比较费劲:
(1) 首先需要用包含如下内容(注意,有五行,每个冒号后面都有一个空格):
Manifest-Version: 1.0
Premain-Class: MyAgent
Boot-Class-Path:
Can-Redefine-Classes: false
的MAINIFEST.MF文件,将上面的代码打为jar包,这里命名为“JavaAgentTest.jar”,并将其放在用户主目录下。
(2) 写一个测试类如下:
/**
* Created by chengxia on 2019/3/24.
*/
public class Test {
public static void main(String []args){
System.out.println("This is main.");
}
}
(3) 指定运行参数
直接运行上面的类,肯定不能调起Java Agent,需要在运行时添加如下参数(VM参数):
-javaagent:/Users/chengxia/JavaAgentTest.jar=Hello1 -javaagent:/Users/chengxia/JavaAgentTest.jar=Hello2
运行的效果如下:
=========premain方法执行========
Hello1
=========premain方法执行========
Hello2
This is main.
Process finished with exit code 0
3.3.2 通过Instrumentation统计对象的内存占内存大小
通过Java代理可以获得Instrumentation类的对象,该对象有getObjectSize()
方法,可以返回一个对象的ShallowSize。至于DeepSize的统计,采取和上面Unsafe类似的思路,实现一个名为SizeOfAgent的java代理。
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 SizeOfAgent
{
private static Instrumentation inst;
/** initializes agent */
public static void premain(String agentArgs, Instrumentation instP)
{
inst = instP;
}
/**
* Returns object size without member sub-objects.
* @param o object to get size of
* @return object size
*/
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);
}
/**
* Calculates full size of object iterating over
* its hierarchy graph.
* @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) {//这个if是bug,应当去掉
// skip interned string
if (obj == ((String) obj).intern()) {
return true;
}
}
return (obj == null) || visited.containsKey(obj);
}
@SuppressWarnings("rawtypes")
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);
// process all array elements
Class clazz = obj.getClass();
if (clazz.isArray())
{
if(clazz.getName().length() != 2)//如果是原生数组,返回[I,代表int型数组
{// skip primitive type array
int length = Array.getLength(obj);
for (int i = 0; i < length; I++)
{
stack.add(Array.get(obj, i));
}
}
return result;
}
// process all fields of the object
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; // skip primitive fields
}
else
{
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;
}
}
在运行程序的时候,加上该代理的参数,这样就可以在程序中调用SizeOfAgent.fullSizeOf()
静态方法获得一个对象占用内存空间的大小了。如下是一个例子:
/**
* Created by chengxia on 2019/3/24.
*/
public class TestSizeOfAgent {
private static class ObjectA {
String str; // 4
int i1; // 4
byte b1; // 1
byte b2; // 1
int i2; // 4
byte b3; // 1
ObjectB obj; //4
}
private static class ObjectB {
}
public static void main(String []args){
try {
System.out.println("int:" + SizeOfAgent.fullSizeOf(new Integer(2)));
System.out.println("str3:" + SizeOfAgent.fullSizeOf("abcd"));
System.out.println("ObjectA:" + SizeOfAgent.fullSizeOf(new ObjectA()));
}catch (Exception e){
e.printStackTrace();
}
}
}
运行结果如下:
int:16
str3:0
ObjectA:32
Process finished with exit code 0
可以看出,和Unsafe得出的统计结果有些不一致。这是因为后面的该方法中,对于常量池中的字符串常量,没有统计内存占用(代码中有特殊处理,因为这种不同对象只存一份,如果都统计会有重复)。