Java Cache-EHCache系列之计算实例占用的内存大小(SizeOf引擎)

简介:
在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中计算一个实例占用内存大小的基本思路和以上类似:遍历实例数上的所有节点,对每个节点计算其占用的内存大小。不过它结构设计的更好,而且它有三种用于计算一个实例占用内存大小的实现。我们先来看这三种用于计算一个实例占用内存大小的逻辑:
  1. ReflectionSizeOf
    使用反射的方式计算计算一个实例占用的内存大小就是我上面想到的这种方法。

    因为使用反射计算一个实例占用内存大小的根据不同虚拟机的特性是来判断一个实例的各个字段占用的大小以及该实例存储额外信息占用的大小,因而EHCache中采用JvmInformation枚举类型来抽象这种对不同虚拟机实现的不同:
    JVM Desc PointerSize JavaPointerSize MinimumObjectSize ObjectAlignment ObjectHeaderSize FieldOffsetAdjustment AgentSizeOfAdjustment
    HotSpot 32-Bit 4 4 8 8 8 0 0
    HotSpot 32-Bit with Concurrent Mark-and-Sweep GC 4 4 16 8 8 0 0
    HotSpot 64-Bit 8 8 8 8 16 0 0
    HotSpot 64-Bit With Concurrent Mark-and-Sweep GC 8 8 24 8 16 0 0
    HotSpot 64-Bit with Compressed OOPs 8 4 8 8 12 0 0
    HotSpot 64-Bit with Compressed OOPs and Concurrent Mark-and-Sweep GC 8 4 24 8 12 0 0
    JRockit 32-Bit 4 4 8 8 16 8 8
    JRockit 64-Bit(with no reference compression) 4 4 8 8 16 8 8
    JRockit 64-Bit with 4GB compressed References 4 4 8 8 16 8 8
    JRockit 64-Bit with 32GB Compressed References 4 4 8 8 16 8 8
    JRockit 64-Bit with 64GB Compressed References 4 4 16 16 24 16 16
    IBM 64-Bit with Compressed References 4 4 8 8 16 0 0
    IBM 64-Bit with no reference compression 8 8 8 8 24 0 0
    IBM 32-Bit 4 4 8 8 16 0 0
    UNKNOWN 32-Bit 4 4 8 8 8 0 0
    UNKNOWN 64-Bit 8 8 8 8 16 0 0

    ObjectAligment default: 8
    MinimumObjectSize default equals ObjectAligment
    ObjectHeaderSize default: PointerSize + JavaPointerSize
    FIeldOffsetAdjustment default: 0
    AgentSizeOfAdjustment default: 0
    ReferenceSize equals JavaPointerSize
    ArrayHeaderSize: ObjectHeaderSize + 4(INT Size)
    JRockit and IBM JVM do not support ReflectionSizeOf


    而对基本类型,则因为虚拟机的规范,它们都是相同的,EHCache中采用PrimitiveType枚举类型来定义不同基本类型的长度:
    enum PrimitiveType {
        BOOLEAN( boolean. class, 1),
        BYTE( byte. class, 1),
        CHAR( char. class, 2),
        SHORT( short. class, 2),
        INT( int. class, 4),
        FLOAT( float. class, 4),
        DOUBLE( double. class, 8),
        LONG( long. class, 8);

         private Class<?> type;
         private  int size;

         public  static  int getReferenceSize() {
             return CURRENT_JVM_INFORMATION.getJavaPointerSize();
        }
         public  static  long getArraySize() {
             return CURRENT_JVM_INFORMATION.getObjectHeaderSize() + INT.getSize();
        }
    }

    反射计算一个实例(instance)占用内存大小(size)步骤如下:
    a. 如果instance为null,size为0,直接返回。
    b. 如果instance是数组类型,size为数组头部大小+每个数组元素占用大小*数组长度+填充到对象对齐最小单位,最后保证如果size要比对象最小大小大过相等。
    c. 如果instance是普通实例,size初始值为对象头部大小,然后找到对象对应类的所有继承类,从最顶层类开始遍历所有类(规则3),对每个类,纪录长整型和双精度型、整型和浮点型、短整型和字符型、布尔型和字节型以及引用类型的非静态字段的个数。如果整型和双精度型字段个数不为0,且当前size没有按长整型的大小对齐(规则5),选择部分其他类型字段排在长整型和双精度型之前,直到填充到以长整型大小对齐,然后按照先规则2的顺序排列个字计算不同类型字段的大小。在每个类之间如果没有按规定大小对齐,则填充缺少的字节(规则4)。在所有类计算完成后,如果没有按照类的对齐方式,则按类对齐规则对齐(规则1)。最后保证一个对象实例的大小要一个对象最小大小要大或相等。

  2. UnsafeSizeOf中
    UnsafeSizeOf的实现比反射的实现要简单的多,它使用Sun内部库的Unsafe类来获取字段的offset值来计算一个类占用的内存大小(个人理解,这个应该只支持Sun JVM,但是怎么JRockit中有对FieldOffsetAdjustment的配置,而该方法只在这个类中被使用。。。)。对数组,它使用Unsafe.arrayBaseOffset()方法返回数组头大小,使用Unsafe.arrayIndexScale()方法返回一个数组元素占用的内存大小,其他计算和反射机制类似。这里在最后计算填充前有对FieldOffsetAdjustment的调整,貌似在JRockit JVM中使用到了,不了解为什么它需要这个调整。对实例大小的计算也比较简单,它首先遍历当前类和父类的所有非静态字段,通过Unsafe.objectFieldOffset()找到最后一个字段的offset,根据之前Java实例内存结构,要找到最后一个字段,只需从当前类到最顶层父类遍历第一个有非静态字段的类的所有非静态字段即可。在找到最后一个字段的offset以后也需要做FieldOffsetAdjustment调整,之后还需要加1(因为有对象对齐大小对齐,因而通过加1而避免考虑最后一个字段类型的问题,很巧妙的代码!)。最后根据规则以对对象以对象对齐大小对齐。

  3. AgentSizeOf
    在Java 1.5以后,提供了Instrumentation接口,可以调用该接口的getObjectSize方法获取一个对象实例占用的内存大小。对Instrumentation的机制不熟,但是从EHCache代码的实现角度上,它首先需要有一个sizeof-agent.jar的包(包含在net.sf.ehcache.pool.sizeof中),在该jar包的MANIFEST.MF文件中指定Premain-Class类,这个类实现两个静态的premain、agentmain方法。在实际运行时,EHCache会将sizeof-agent.jar拷贝到临时文件夹中,然后调用Sun工具包中的VirtualMachine的静态attach方法,获取一个VirtualMachine实例,然后调用其实例方法loadAgent方法,传入sizeof-agent.jar文件全路径,即可将一个SizeOfAgent类附着到当前实例中,而我们就可以通过SizeOfAgent类来获取它的Instrumentation实例来计算一个实例的大小。
我们可以使用一下一个简单的例子来测试一下各种不同计算方法得出的结果: 
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<Field> filterFields(Class<?> klazz, Collection<Field> fields);
     //  Checks whether the type needs to be filtered
     boolean filterClass(Class<?> klazz);
}
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);
}

还可以参考

http://www.javamex.com/tutorials/memory/

相关文章
|
22天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
24 0
|
1天前
|
存储 缓存 资源调度
阿里云服务器经济型、通用算力型、计算型、通用型、内存型实例区别与选择指南
在我们通过阿里云的活动选购云服务器的时候会发现,相同配置的云服务器往往有多个不同的实例可选,而且价格差别也比较大,这会是因为不同实例规格的由于采用的处理器不同,底层架构也有所不同(例如X86 计算架构与Arm 计算架构),因此不同实例的云服务器其性能与适用场景是有所不同。本文将详细解析阿里云的经济型、通用算力型、计算型、通用型和内存型实例的性能特点及适用场景,帮助用户根据自己的业务需求做出明智的选择。
|
24天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
37 8
|
22天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
24天前
|
存储 分布式计算 安全
阿里云服务器经济型e、通用算力型u1、计算型c8i、通用型g8i、内存型r8i实例介绍与选择参考
在阿里云现在的活动中,可选的云服务器实例规格主要有经济型e、通用算力型u1、计算型c8i、通用型g8i、内存型r8i实例,虽然阿里云在活动中提供了多种不同规格的云服务器实例,以满足不同用户和应用场景的需求。但是有的用户并不清楚他们的性能如何,应该如何选择。本文将详细介绍阿里云服务器中的经济型e、通用算力型u1、计算型c8i、通用型g8i、内存型r8i实例的性能、适用场景及选择参考,帮助用户根据自身需求做出合适的选择。
|
24天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
24天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
21天前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
36 0
|
1月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
291 1
|
1月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80

热门文章

最新文章