最近自己在做项目的时候,因为有部分的内容会存储到本地内存,但是担心占用过大,这个时候就需要计算我使用的内存的大小,防止占用过大。
之前接触C,C可以很方便的操作内存,程序开始的时候就可以分配一大块内存供某个需求使用,如果不够了可以进行某些方法进行业务回收等等。但是Java是由Java虚拟机进行自动分配和回收内存,不建议上层直接使用,虽然也提供了unsafe的使用,直接操作内存,但是毕竟不太安全。所以目前在java上预分配一块内存似乎并不太容易实现。
我们最终还是回到每次计算我们某个结构的大小来看内存的使用情况。我们都知道,Java的Class是基本的表示结构,所以我们计算大小得先从这个基本的Class看起。
下面我们来看看一段代码:
public static void main(String[] aa) {
Num c = new Num();
c.num = 0xffff;
c.num2 = 0xcccc;
c.string = "333";
Class<?> clazz = c.getClass();
long d = 0;
try {
d = unsafe.objectFieldOffset(clazz.getDeclaredField("string")) + 4;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
for(int i=0;i<d;i++) {
System.out.println( (0xff & unsafe.getByte(c, i)));
}
}
public static class Num{
public int num;
public int num2;
public String string ;
}
结果如下:
1
0
0
0
0
0
0
0
67
193
0
248
255
255
0
0
204
204
0
0
135
82
91
237
单从这个结果看,我们能看到什么,你可以猜猜这个Class的内存结构是怎么分布的。
有些字段还是不明所以,于是网上找些资料来参考下,发现一个类实例的内存模型如下:
名称 | 占用字节 |
---|---|
Class头 | 8字节 |
oop指针 | 4字节 |
数据区域 | 不定 |
对其补充 | 补充到整个大小为8字节的倍数 |
是的,整个class的内存使用就是这个表展示的,我们分析下上面的数据,对照一下看看。
1 --
0 |
0 |
0 | class头
0 |
0 |
0 |
0 --
67 --
193 | oop指针
0 |
248 --
255 --
255 | num字段(int型)
0 |
0 --
204 --
204 | num2字段(int型)
0 |
0 --
135 --
82 | Class类型(4字节的指针地址)
91 |
237 --
通过网上查询,我们可以简单看下这个表各个字段的含义,第一个Class头的8个字节:这个字节存储了比如这个实例目前的锁信息、目前属于的堆类型,初始化进度等等。第二个区域,oop指针,这个字段存储的是这个类的定义,就比如Java反射可以拿到字段名称,方法名称这些值都是存储在这个指针所指向的定义中。第三个区域,数据区域,存放数据的区域,这里的结构区分主要是两种:数组和非数组。如果是数组,数据区域中还会包含这个数组的大小。
我们大概说明了Class内存结构模型,这个时候对计算大小有个基本的方案了。我们可以根据Class定义的结构进行计算此实例占用的大小了。
Class 头和oop是固定的12字节,后面的基本类型,如int,long,char,boolean会根据不同的类型占据不同的字节空间,所有继承自Object的类型都使用指针指向对应的又一个Class实例的内存模型的地址。
我们刚才举出来的例子Class中的都是简单的结构,但是如果是定义的Class中有定义的其他Class作为它的成员变量。那我们应该怎么处理?方案很简答,如果你需要计算左右的占用空间,就把成员变量占用的实际空间也计算出来,如果还有变量的变量,就继续向深程度计算,最后所有的相加,即为我们需要的值。
Java有一个比较好的key-value格式的本地缓存项目ehcache,作为这个项目比较重要的监控就是内存占用,防止缓存使用过大。而子项目# sizeof 作为这个项目的重要插件,就是为了做这个内存大小占用的计算的。这里简单的给大家介绍这个项目。
SizeOf sizeOf = SizeOf.newInstance(); (1)
long shallowSize = sizeOf.sizeOf(someObject); (2)
long deepSize = sizeOf.deepSizeOf(someObject); (3)
这3行的使用方法基本包含了这个项目的所有内容,1.计算一层内存占用大小,2.计算所有引用关系包含的内存占用。
newInstance的初始化里面初始化了3种不同的计算Class实例大小的方法:
- AgentSizeOf : 使用jvm代理和Instrumentation
- UnsafeSizeOf : 使用unsafe
- ReflectionSizeOf : 通过反射出来Class的成员,通过成员类型进行计算
按照这个顺序进行优先初始化,如果失败了才会使用后面的方法。从前到后也是建议的使用顺序,反射的效率是最低的。
还有一大重要内容就是deepSizeOf是如何把这些Class的大小加起来的。可以看看下面这个函数。
long walk(VisitorListener visitorListener, Object... root) {
long result = 0;
Deque<Object> toVisit = new ArrayDeque<>();
IdentityHashMap<Object, Object> visited = new IdentityHashMap<>();
if (root != null) {
if (traversalDebugMessage != null) {
traversalDebugMessage.append("visiting ");
}
for (Object object : root) {
nullSafeAdd(toVisit, object);
}
}
while (!toVisit.isEmpty()) {
Object ref = toVisit.pop();
if (visited.containsKey(ref)) {
continue;
}
Class<?> refClass = ref.getClass();
if (!byPassIfFlyweight(ref) && shouldWalkClass(refClass)) {
if (refClass.isArray() && !refClass.getComponentType().isPrimitive()) {
for (int i = 0; i < Array.getLength(ref); i++) {
nullSafeAdd(toVisit, Array.get(ref, i));
}
} else {
for (Field field : getFilteredFields(refClass)) {
try {
nullSafeAdd(toVisit, field.get(ref));
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}
}
final long visitSize = visitor.visit(ref);
if (visitorListener != null) {
visitorListener.visited(ref, visitSize);
}
result += visitSize;
}
visited.put(ref, null);
}
return result;
}
函数的第二个参数为需要计算的class的实例,一个Object,可以传入任何自定义的Class实例。从Deque的定义可以看到这里用的是栈的方法取代递归来计算deepSizeof。Deque命名的toVisit代表还需要计算的类型的大小,一个实例从这个Deque取出来后,将这个大小记录上,同时将这个实例包含的需要放入的成员实例放进这个Deque中。继续进行向下分析,直到整个分析完为止。另外逻辑中visited定义出来为了标记已经计算过的实例,因为可能引用的是同一实例。整个逻辑用这一个函数就可以表达出来。
这个项目的分析了这两块内容,基本也就没有其他的内容了,如果大家感兴趣,可以下载源码,希望大家多多交流。
网友评论