Ehcache计算Java对象内存大小

在EHCache中,可以设置maxBytesLocalHeap、maxBytesLocalOffHeap、maxBytesLocalDisk值,以控制Cache占用的内存、磁盘的大小(注:这里Off Heap是指Element中的值已被序列化,但是还没写入磁盘的状态,貌似只有企业版的EHCache支持这种配置;而这里maxBytesLocalDisk是指在最大在磁盘中的数据大小,而不是磁盘文件大小,因为磁盘文中有一些数据是空闲区),因而EHCache需要有一种机制计算一个类在内存、磁盘中占用的字节数,其中在磁盘中占用的字节大小计算比较容易,只需要知道序列化后字节数组的大小,并且加上一些统计信息,如过期时间、磁盘位置、命中次数等信息即可,而要计算一个对象实例在内存中占用的大小则要复杂一些。

计算一个实例内存占用大小思路
Java中,除了基本类型,其他所有通过字段包含其他实例的关系都是引用关系,因而我们不能直接计算该实例占用的内存大小,而是要递归的计算其所有字段占用的内存大小的和。在Java中,我们可以将所有这些通过字段引用简单的看成一种树状结构,这样就可以遍历这棵树,计算每个节点占用的内存大小,所有这些节点占用的内存大小的总和就当前实例占用的内存大小,遍历的算法有:先序遍历、中序遍历、后序遍历、层级遍历等。但是在实际情况中很容易出现环状引用(最简单的是两个实例之间的直接引用,还有是多个实例构成的一个引用圈),而破坏这种树状结构,而让引用变成图状结构。然而图的遍历相对比较复杂(至少对我来说),因而我更愿意把它继续看成一颗树状图,采用层级遍历,通过一个IdentitySet纪录已经计算过的节点(实例),并且使用一个Queue来纪录剩余需要计算的节点。算法步骤如下:
1. 先将当前实例加入Queue尾中。
2. 循环取出Queue中的头节点,计算它占用的内存大小,加到总内存大小中,并将该节点添加到IdentitySet中。
3. 找到该节点所有非基本类型的子节点,对每个子节点,如果在IdentityMap中没有这个子节点的实例,则将该实例加入的Queue尾。
4. 回到2继续计算直到Queue为空。
剩下的问题就是如何计算一个实例本身占用的内存大小了。这个以我目前的经验,我只能想到遍历一个实例的所有实例字段,根据每个字段的类型来判断每个字段占用的内存大小,然后它们的和就是该实例占用的总内存的大小。对于字段的类型,首先是基本类型字段,byte、boolean占一个字节,short、char占2个字节,int、float占4个字节,double占8个字节等;然后是引用类型,对类型,印象中虚拟机规范中没有定义其大小,但是一般来说对32位系统占4个字节,对64位系统占8个字节;再就是对数组,基本类型的数组,byte每个元素占1个字节,short、char每个元素占2个字节,int每个元素占4个字节,double每个元素占8个字节,引用类型的数组,先计算每个引用元素占用的字节数,然后是引用本省占用的字节数。
以上是我对EHCache中计算一个实例逻辑不了解的时候的个人看法,那么接下来我们看看EHCache怎么来计算。

Java对象内存结构(以Sun JVM为例)
参考:http://www.importnew.com/1305.html,之所以把参考链接放在开头是因为下面基本上是对链接所在文章的整理,之所以要整理一遍,一是怕原链接文章消失,二则是为了加深自己的理解。
在Sun JVM中,除数组以外的对象都有8个字节的头部(数组还有额外的4个字节头部用于存放长度信息),前面4个字节包含这个对象的标识哈希码以及其他一些flag,如锁状态、年龄等标识信息,后4个字节包含一个指向对象的类实例(Class实例)的引用。在这头部8个字节之后的内存结构遵循一下5个规则:
规则1: 任何对象都是以8个字节为粒度进行对齐的。
比如对一个Object类,因为它没有任何实例,因而它只有8个头部直接,则它占8个字节大小。而对一个只包含一个byte字段的实例,它需要填上(padding)7个字节的大小,因而它占16个字节,典型的如一个Boolean实例要占用16个字节的内存!

class MyClass {
byte a;
}
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 7 bytes] 16

规则2: 类属性按照如下优先级进行排列:长整型和双精度类型;整型和浮点型;字符和短整型;字节类型和布尔类型;最后是引用类型。这些属性都按照各自的单位对齐。
在Java对象内存结构中,对象以上述的8个字节的头部开始,然后对象属性紧随其后。为了节省内存,Sun VM并没有按照属性声明时顺序来进行内存布局,而是使用如下顺序排列:
1. 双精度型(double)和长整型(long),8字节。
2. 整型(int)和浮点型(float),4字节。
3. 短整型(short)和字符型(char),2字节。
4. 布尔型(boolean)和字节型(byte),2字节。
5. 引用类型。
并且对象属性总是以它们的单位对齐,对于不满4字节的数据类型,会填充未满4字节的部分。之所以要填充是出于性能考虑:因为从内存中读取4字节数据到4字节寄存器的动作,如果数据以4字节对齐的情况小,效率要高的多。

class MyClass {
byte a;
int c;
boolean d;
long e;
Object f;
}
//如果JVM不对其重排序,它要占40个字节
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[c: 4 bytes] 16
[d: 1 byte ] 17
[padding: 7 bytes] 24
[e: 8 bytes] 32
[f: 4 bytes] 36
[padding: 4 bytes] 40
//经JVM重排序后,只需要占32个字节
[HEADER: 8 bytes] 8
[e: 8 bytes] 16
[c: 4 bytes] 20
[a: 1 byte ] 21
[d: 1 byte ] 22
[padding: 2 bytes] 24
[f: 4 bytes] 28
[padding: 4 bytes] 32

规则3: 不同类继承关系中的成员不能混合排列。首先按照规则2处理父类中的成员,接着才是子类的成员。

class A {
long a;
int b;
int c;
}
class B extends A {
long d;
}
[HEADER: 8 bytes] 8
[a: 8 bytes] 16
[b: 4 bytes] 20
[c: 8 bytes] 32

规则4: 当父类最后一个属性和子类第一个属性之间间隔不足4字节时,必须扩展到4个字节的基本单位。

class A {
byte a;
}
class B extends A {
byte b;
}
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[b: 1 byte ] 13
[padding: 3 bytes] 16

规则5: 如果子类第一个成员时一个双精度或长整型,并且父类没有用完8个字节,JVM会破坏规则2,按整型(int)、短整型(short)、字节型(byte)、引用类型(reference)的顺序向未填满的空间填充。

class A {
byte a;
}
class B extends A {
long b;
short c;
byte d;
}
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[c: 2 bytes] 14
[d: 1 byte ] 15
[padding: 8 bytes] 24

数组内存布局
数组对象除了作为对象而存在的头以外,还存在一个额外的头部成员用来存放数组的长度,它占4个字节。

//三个元素的字节数组
[HEADER: 12 bytes] 12
[[0]: 1 byte ] 13
[[1]: 1 byte ] 14
[[2]: 1 byte ] 15
[padding: 1 byte ] 16
//三个元素的长整型数组
[HEADER: 12 bytes] 12
[padding: 4 bytes ] 16
[[0]: 8 bytes] 24
[[1]: 8 bytes] 32
[[2]: 8 bytes] 40

非静态内部类
非静态内不累它又一个额外的”隐藏”成员,这个成员时一个指向外部类的引用变量。这个成员是一个普通引用,因此遵循引用内存布局的规则。因此内部类有4个字节的额外开销。

EHCache计算一个实例占用的内存大小
EHCache中计算一个实例占用内存大小的基本思路和以上类似:遍历实例数上的所有节点,对每个节点计算其占用的内存大小。不过它结构设计的更好,而且它有三种用于计算一个实例占用内存大小的实现。我们先来看这三种用于计算一个实例占用内存大小的逻辑:

我们可以使用一下一个简单的例子来测试一下各种不同计算方法得出的结果:

public class EhcacheSizeOfTest {
public static void main(String[] args) {
MyClass ins = new MyClass();

System.out.println(“ReflectionSizeOf: ” + calculate(new ReflectionSizeOf(), ins));
System.out.println(“UnsafeSizeOf: ” + calculate(new UnsafeSizeOf(), ins));
System.out.println(“AgentSizeOf: ” + calculate(new AgentSizeOf(), ins));
}

private static long calculate(SizeOf sizeOf, Object instance) {
return sizeOf.sizeOf(instance);
}

public static class MyClass {
byte a;
int c;
boolean d;
long e;
Object f;
}
}
//输出结果如下(问题:这里的JVM是64-Bit HotSpot JVM with Compressed OOPs,它的实例头部占用了12个字节大小,但是它占用内存的大小还是和32位的大小一样,这是为什么?):
[31 23:21:19,598 INFO ] [main] sizeof.JvmInformation – Detected JVM data model settings of: 64-Bit HotSpot JVM with Compressed OOPs
ReflectionSizeOf: 32
UnsafeSizeOf: 32
[31 23:26:52,479 INFO ] [main] sizeof.AgentLoader – Located valid ‘tools.jar’ at ‘C:\Program Files\Java\jdk1.7.0_25\jre..\lib\tools.jar’
[31 23:26:52,729 INFO ] [main] sizeof.AgentLoader – Extracted agent jar to temporary file C:\Users\DINGLE~1\AppData\Local\Temp\ehcache-sizeof-agent6171098352070763093.jar
[31 23:26:52,729 INFO ] [main] sizeof.AgentLoader – Trying to load agent @ C:\Users\DINGLE~1\AppData\Local\Temp\ehcache-sizeof-agent6171098352070763093.jar
AgentSizeOf: 32

Deep SizeOf计算
EHCache中的SizeOf类中还提供了deepSize计算,它的步骤是:使用ObjectGraphWalker遍历一个实例的所有对象引用,在遍历中通过使用传入的SizeOfFilter过滤掉那些不需要的字段,然后调用传入的Visitor对每个需要计算的实例做计算。
ObjectGraphWalker的实现算法和我之前所描述的类似,稍微不同的是它使用了Stack,我更倾向于使用Queue,只是这个也只是影响遍历的顺序,这里有点深度优先还是广度优先的味道。另外,它抽象了SizeOfFilter接口,可以用于过滤掉一些不想用于计算内存大小的字段,如Element中的key字段。SizeOfFilter提供了对类和字段的过滤:

public interface SizeOfFilter {
// Returns the fields to walk and measure for a type
Collection

SizeOfFilter的实现类可以用于过滤过滤掉@IgnoreSizeOf注解的字段和类,以及通过net.sf.ehcache.sizeof.filter系统变量定义的文件,读取其中的每一行为包名或字段名作为过滤条件。最后,为了性能考虑,它对一些计算结果做了缓存。

ObjectGraphWalker中,它还会忽略一些系统原本就存在的一些静态变量以及类实例,所有这些信息都定义在FlyweightType类中。

SizeOfEngine类
SizeOfEngine是EHCache中对使用不同方式做SizeOf计算的抽象,如在计算内存中对象的大小需要使用SizeOf类来实现,而计算磁盘中数据占用的大小直接使用其size值即可,因而在EHCache中对SizeOfEngine有两个实现:DefaultSizeOfEngine和DiskSizeOfEngine。对DiskSizeOfEngine比较简单,其container参数必须是DiskMarker类型,并且直接返回其size字段即可;对DefaultSizeOfEngine,则需要配置SizeOfFilter和SizeOf子类实现问题,对SizeOfFilter,它会默认加入AnnotationSizeOfFilter、使用builtin-sizeof.filter文件中定义的类、字段配置的ResourceSizeOfFilter、用户通过net.sf.ehcache.sizeof.filter配置的filter文件的ResourceSizeOfFilter;对SizeOf的子类实现问题,它优先选择AgentSizeOf,如果不支持则使用UnsafeSizeOf,最后才使用ReflectionSizeOf。

public interface SizeOfEngine {
Size sizeOf(Object key, Object value, Object container);
SizeOfEngine copyWith(int maxDepth, boolean abortWhenMaxDepthExceeded);
}

还可以参考

Original: https://www.cnblogs.com/E-star/p/10222258.html
Author: E_star
Title: Ehcache计算Java对象内存大小

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/537096/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球