尽管优化应用程序代码的内存使用率的主题不是新话题,但它并不是众所周知的。 本文简要介绍了Java进程的内存使用情况,然后深入研究了您编写的Java代码的内存使用情况。 最后,它展示了使应用程序代码的存储效率更高的方法,特别是在使用Java集合(例如HashMap
和ArrayList
。
背景:Java进程的内存使用情况
通过在命令行上执行java
或启动某些基于Java的中间件来运行Java应用程序时,Java运行时会创建一个操作系统进程,就像运行基于C的程序一样。 实际上,大多数JVM基本上都是用C或C ++编写的。 作为操作系统进程,Java运行时与任何其他进程一样在内存方面面临相同的限制:体系结构提供的可寻址性和操作系统提供的用户空间。
该体系结构提供的内存可寻址性取决于处理器的位大小-例如32位或64位,或者在大型机的情况下为31位。 进程可以处理的位数决定了处理器可以寻址的内存范围:32位提供2 ^ 32的可寻址范围,即4,294,967,296位或4GB。 64位处理器的可寻址范围要大得多:2 ^ 64是18,446,744,073,709,551,616或16艾字节。
操作系统本身将处理器体系结构提供的某些可寻址范围用于其内核,(对于使用C或C ++编写的JVM)将其用于C运行时。 OS和C运行时使用的内存量取决于所使用的OS,但通常很重要:Windows的默认使用量为2GB。 剩余的可寻址空间(称为用户空间 )是正在运行的实际进程可用的内存。
对于Java应用程序,用户空间是Java进程使用的内存,实际上由两个池组成:Java堆和本机 (非Java)堆。 Java堆的大小由JVM的Java堆设置控制: -Xms
和-Xmx
设置最小和最大Java堆。 本机堆是在以最大大小设置分配Java堆之后剩余的用户空间。 图1显示了32位Java进程的外观示例:
图1. 32位Java进程的示例内存布局
在图1中 ,OS和C运行时使用了4GB可寻址范围中的大约1GB,Java堆使用了将近2GB,而本机堆使用了其余空间。 请注意,JVM本身使用内存(与OS内核和C运行时相同),并且JVM使用的内存是本机堆的子集。
Java对象的剖析
当您的Java代码使用new
运算符创建Java对象的实例时,分配的数据比您预期的要多得多。 例如,您可能会惊讶地发现一个int
值与一个Integer
对象(可以容纳int
值的最小对象)的大小比通常为1:4。 额外的开销是JVM用于描述Java对象的元数据,在本例中为Integer
。
对象元数据的数量随JVM版本和供应商的不同而不同,但通常包括:
- Class :指向类信息的指针,它描述对象的类型。 例如,对于
java.lang.Integer
对象,这是指向java.lang.Integer
类的指针。 - 标志 :描述对象状态的标志的集合,包括对象的哈希码(如果有的话)以及对象的形状 (即对象是否为数组)。
- Lock :对象的同步信息,即对象当前是否同步。
然后,对象元数据后跟对象数据本身,该对象数据由存储在对象实例中的字段组成。 对于java.lang.Integer
对象,这是一个int
。
因此,当您在运行32位JVM时创建java.lang.Integer
对象的实例时,该对象的布局可能如图2所示:
图2. 32位Java流程的java.lang.Integer
对象的示例布局
如图2所示,因为对象元数据使用了这128位中的其余部分,所以将128位数据用于存储int
值中的32位数据。
Java数组对象的剖析
数组对象(例如int
值的数组)的形状和结构与标准Java对象的形状和结构相似。 主要区别在于,数组对象还有一条额外的元数据,它表示数组的大小。 因此,数组对象的元数据包括:
- Class :指向类信息的指针,它描述对象的类型。 对于
int
字段数组,这是指向int[]
类的指针。 - 标志 :描述对象状态的标志的集合,包括对象的哈希码(如果有的话)以及对象的形状(即对象是否为数组)。
- Lock :对象的同步信息,即对象当前是否同步。
- Size :数组的大小。
图3显示了一个int
数组对象的示例布局:
图3. 32位Java流程的int
数组对象的示例布局
在图3中 ,因为数组元数据使用这160位中的其余部分,所以160位数据将32位数据存储在int
值中。 对于诸如byte
, int
和long
基元,就内存而言,单入口数组比单字段对应的包装对象( Byte
, Integer
或Long
)昂贵。
更加复杂的数据结构剖析
良好的面向对象设计和编程鼓励使用封装 (提供控制数据访问的接口类)和委派 (使用帮助对象执行任务)。 封装和委派使大多数数据结构的表示涉及多个对象。 一个简单的示例是java.lang.String
对象。 java.lang.String
对象中的数据是由java.lang.String
对象封装的字符数组,该对象管理和控制对字符数组的访问。 32位Java进程的java.lang.String
对象的布局可能类似于图4:
图4. 32位Java流程的java.lang.String
对象的示例布局
如图4所示,除了标准对象元数据外, java.lang.String
对象还包含一些用于管理字符串数据的字段。 通常,这些字段是哈希值,字符串大小的计数,字符串数据的偏移量以及对字符数组本身的对象引用。
这意味着要具有8个字符的字符串(128位char
数据),则256位数据用于字符数组,而224位数据用于管理该字符的java.lang.String
对象,总共480个位(60字节)代表128位(16字节)数据。 这是3.75:1的间接费用比率。
通常,数据结构越复杂,其开销就越大。 下一节将对此进行详细讨论。
32位和64位Java对象
前面示例中对象的大小和开销适用于32位Java进程。 正如您从“ 背景:Java进程部分的内存使用情况”中了解到的那样,64位处理器的内存可寻址性级别比32位处理器高得多。 使用64位进程时,Java对象中某些数据字段的大小(特别是对象元数据和引用另一个对象的任何字段)的大小也需要增加到64位。 其他数据字段类型(例如int
, byte
和long
的大小不会改变。 图5显示了一个64位Integer
对象和一个int
数组的布局:
图5.用于64位Java进程的java.lang.Integer
对象和int
数组的示例布局
图5显示,对于64位Integer
对象,现在使用224位数据存储用于int
字段的32位-开销比为7:1。 对于64位单元素int
数组,将使用288位数据存储32位int
条目-开销为9:1。 此操作对实际应用程序的影响是,将以前运行在32位Java运行时上的应用程序移至64位Java运行时时,其Java堆内存使用量显着增加。 通常,增加量约为原始堆大小的70%。 例如,使用1GB Java堆和32位Java运行时的Java应用程序通常将使用1.7GB Java堆和64位Java运行时。
请注意,这种内存增加并不限于Java堆。 本机堆内存区域的使用量也会增加,有时会增加90%。
表1显示了当应用程序以32位和64位模式运行时对象和数组的字段大小:
表1. 32位和64位Java运行时的对象中的字段大小
栏位类型 | 字段大小(位) | |||
---|---|---|---|---|
目的 | 数组 | |||
32位 | 64位 | 32位 | 64位 | |
boolean | 32 | 32 | 8 | 8 |
byte | 32 | 32 | 8 | 8 |
char | 32 | 32 | 16 | 16 |
short | 32 | 32 | 16 | 16 |
int | 32 | 32 | 32 | 32 |
float | 32 | 32 | 32 | 32 |
long | 64 | 64 | 64 | 64 |
double | 64 | 64 | 64 | 64 |
对象字段 | 32 | 64(32 *) | 32 | 64(32 *) |
对象元数据 | 32 | 64(32 *) | 32 | 64(32 *) |
*通过压缩参考或压缩OOP技术,对象字段和用于每个对象元数据条目的数据的大小可以减少到32位。
压缩参考和压缩普通对象指针(OOP)
IBM和Oracle JVM都分别通过压缩引用( -Xcompressedrefs
)和压缩-Xcompressedrefs
( -XX:+UseCompressedOops
)选项提供了对象引用压缩功能。 使用这些选项可以将对象字段和对象元数据值存储为32位而不是64位。 当应用程序从32位Java运行时移动到64位Java运行时时,这样做的效果是使Java堆内存增加了70%。 注意,这些选项对本机堆的内存使用没有影响; 使用64位Java运行时,它仍然比使用32位Java运行时更高。
Java集合的内存使用情况
在大多数应用程序中,使用作为核心Java API一部分提供的标准Java Collections类来存储和管理大量数据。 如果内存占用空间优化对您的应用程序很重要,那么了解每个集合提供的功能以及相关的内存开销特别有用。 通常,集合功能功能的级别越高,其内存开销就越高—因此,使用提供了比您所需功能更多的功能的集合类型将导致不必要的额外内存开销。
一些常用的集合是:
除了HashSet
,此列表按功能和内存开销的降序排列。 (作为HashMap
对象的包装器的HashSet
实际上提供的功能比HashMap
少,但要稍大一些。)
Java集合: HashSet
HashSet
是Set
接口的实现。 Java Platform SE 6 API文档将HashSet
描述为:
不包含重复元素的集合。 更正式地讲,集合不包含元素对e1和e2,使得e1.equals(e2)最多包含一个空元素。 顾名思义,此接口对数学集合抽象进行建模。
HashSet
的功能比HashMap
少,因为它不能包含多个空条目,并且不能有重复的条目。 该实现是HashMap
的包装, HashSet
对象管理允许放入HashMap
对象的内容。 限制HashMap
功能的附加功能意味着HashSet
的内存开销略高。
图6显示了32位Java运行时上HashSet
的布局和内存使用情况:
图6. HashSet
在32位Java运行时上的内存使用情况和布局
图6显示了java.util.HashSet
对象的浅堆 (单个对象的内存使用)(以字节为单位)以及保留堆 (单个对象及其子对象的内存使用)(以字节为单位)。 浅堆大小为16个字节,保留堆大小为144个字节。 创建HashSet
,其默认容量 (可放入集合中的条目数)为16个条目。 当以默认容量创建HashSet
且未将任何条目放入集合中时,它占用144个字节。 这比HashMap
的内存使用量多了16个字节。 表2显示了HashSet
的属性:
表2. HashSet
属性
属性 | 描述 |
---|---|
默认容量 | 16条目 |
空尺寸 | 144字节 |
高架 | 16个字节加上HashMap 开销 |
1万个收藏的开销 | 16个字节加上HashMap 开销 |
搜索/插入/删除效果 | O(1)—花费的时间是恒定时间,与元素的数量无关(假设没有哈希冲突) |
Java集合: HashMap
HashMap
是Map
接口的实现。 Java Platform SE 6 API文档将HashMap
描述为:
将键映射到值的对象。 映射不能包含重复的键; 每个键最多可以映射到一个值。
HashMap
提供了一种存储键/值对的方法,该方法使用哈希函数将键转换为存储键/值对的集合中的索引。 这样可以快速访问数据位置。 空条目和重复条目是允许的; 这样, HashMap
是HashSet
的简化。
HashMap
的实现是作为HashMap$Entry
对象的数组。 图7显示了32位Java运行时上HashMap
的内存使用情况和布局:
图7. 32位Java运行时上的HashMap
内存使用情况和布局
如图7所示,创建HashMap
时,结果是一个HashMap
对象和一个HashMap$Entry
对象数组,其默认容量为16个条目。 当HashMap
完全为空时,它的大小为128个字节。 插入到HashMap
中的任何键/值对都由HashMap$Entry
对象包装,该对象本身有一些开销。
HashMap$Entry
对象的大多数实现都包含以下字段:
-
int KeyHash
-
Object next
-
Object key
-
Object value
一个32字节的HashMap$Entry
对象管理放入集合中的数据的键/值对。 这意味着HashMap
的总开销由HashMap
对象, HashMap$Entry
数组条目和每个条目的HashMap$Entry
对象组成。 这可以用以下公式表示:
HashMap
对象+数组对象开销+(条目数*(HashMap$Entry
数组条目+HashMap$Entry
对象))
对于10,000个条目的HashMap
,仅HashMap
, HashMap$Entry
数组和HashMap$Entry
对象的开销约为360K。 这是在考虑键的大小和要存储的值之前。
表3显示了HashMap
的属性:
表3. HashMap
属性
属性 | 描述 |
---|---|
默认容量 | 16条目 |
空尺寸 | 128字节 |
高架 | 64字节加上每个条目36字节 |
1万个收藏的开销 | 约36万 |
搜索/插入/删除效果 | O(1)—花费的时间是恒定时间,与元素的数量无关(假设没有哈希冲突) |
Java集合: Hashtable
像HashMap
一样, Hashtable
是Map
接口的实现。 Java平台SE 6 API文档对Hashtable
的描述为:
此类实现一个哈希表,该哈希表将键映射到值。 任何非空对象都可以用作键或值。
Hashtable
与HashMap
非常相似,但是有两个限制。 它不能接受键或值条目的空值,并且它是一个同步的集合。 相反, HashMap
可以接受空值,并且不同步,但是可以使用Collections.synchronizedMap()
方法使其同步。
Hashtable
的实现(也类似于HashMap
的实现)是作为条目对象的数组,在本例中为Hashtable$Entry
对象。 图8显示了32位Java运行时上的Hashtable
表的内存使用情况和布局:
图8. 32位Java运行时上的Hashtable
内存使用情况和布局
图8显示了创建Hashtable
表时,结果是一个Hashtable
对象,该Hashtable
对象使用40个字节的内存以及一个默认容量为11个条目的Hashtable$entry
数组,一个空的Hashtable
的大小总计104个字节。
Hashtable$Entry
有效地存储与HashMap
相同的数据:
-
int KeyHash
-
Object next
-
Object key
-
Object value
这意味着, Hashtable$Entry
对象也是在键/值输入32个字节Hashtable
,并且计算为Hashtable
一个10K条目集合(约360K)的开销,并且大小类似于HashMap
的。
表4显示了Hashtable
的属性:
表4. Hashtable
表的属性
属性 | 描述 |
---|---|
默认容量 | 11条目 |
空尺寸 | 104字节 |
高架 | 56个字节,每个条目36个字节 |
1万个收藏的开销 | 约36万 |
搜索/插入/删除效果 | O(1)—花费的时间是恒定时间,与元素的数量无关(假设没有哈希冲突) |
如您所见, Hashtable
默认容量比HashMap
略小(11 vs. 16)。 否则,主要区别在于Hashtable
不能接受空键和值,以及它的默认同步(可能不需要),这会降低集合的性能。
Java集合: LinkedList
LinkedList
是List
接口的链表实现。 Java Platform SE 6 API文档将LinkedList
描述为:
有序集合(也称为序列)。 该界面的用户可以精确控制列表中每个元素的插入位置。 用户可以通过其整数索引(列表中的位置)访问元素,并在列表中搜索元素。 与集合不同,列表通常允许重复的元素。
该实现是LinkedList$Entry
对象的链接列表。 图9显示了32位Java运行时上LinkedList
的内存使用情况和布局:
图9. 32位Java运行时上的LinkedList
内存使用情况和布局
图9显示创建LinkedList
时,结果是一个LinkedList
对象,它使用24个字节的内存以及一个LinkedList$Entry
对象,一个空的LinkedList
总共需要48个字节的内存。
链接列表的优点之一是它们的大小准确,不需要调整大小。 默认容量实际上是一个条目,并且随着增加或删除更多条目而动态增加和缩小。 每个LinkedList$Entry
对象的开销仍然LinkedList$Entry
:
-
Object previous
-
Object next
-
Object value
但这比HashMap
和Hashtable
的开销要小,因为链接列表仅存储单个条目,而不存储键/值对,并且不需要存储哈希值,因为不使用基于数组的查找。 不利的一面是,对链表的查找会慢得多,因为必须遍历链表才能找到正确的条目。 对于大型链表,这可能会导致较长的查找时间。
表5显示了LinkedList
的属性:
表5. LinkedList
属性
属性 | 描述 |
---|---|
默认容量 | 1项 |
空尺寸 | 48字节 |
高架 | 24个字节,每个条目加上24个字节 |
1万个收藏的开销 | 〜240K |
搜索/插入/删除效果 | O(n)-花费的时间与元素数线性相关 |
Java集合: ArrayList
ArrayList
是List
接口的可调整大小的数组实现。 Java Platform SE 6 API文档将ArrayList
描述为:
有序集合(也称为序列)。 该界面的用户可以精确控制列表中每个元素的插入位置。 用户可以通过其整数索引(列表中的位置)访问元素,并在列表中搜索元素。 与集合不同,列表通常允许重复的元素。
与LinkedList
不同, ArrayList
是使用Object
数组实现的。 图10显示了32位Java运行时上的ArrayList
的内存使用情况和布局:
图10. 32位Java运行时上的ArrayList
内存使用情况和布局
图10显示创建一个ArrayList
时,结果是一个使用32字节内存的ArrayList
对象,以及一个默认大小为10的Object
数组,一个空ArrayList
总共有88字节内存,这意味着ArrayList
不是大小正确,因此具有默认容量,恰好是10个条目。
表6显示了ArrayList
属性:
表6. ArrayList
属性
属性 | 描述 |
---|---|
默认容量 | 10 |
空尺寸 | 88字节 |
高架 | 48个字节,每个条目4个字节 |
10K收集的开销 | 约40K |
搜索/插入/删除效果 | O(n)-花费的时间与元素数成线性关系 |
其他类型的“收藏”
除标准集合外, StringBuffer
还可以被视为集合,因为它可以管理字符数据,并且在结构和功能上与其他集合相似。 Java Platform SE 6 API文档将StringBuffer
描述为:
线程安全的可变字符序列。每个字符串缓冲区都有一个容量。 只要字符串缓冲区中包含的字符序列的长度不超过容量,就不必分配新的内部缓冲区数组。 如果内部缓冲区溢出,则会自动变大。
StringBuffer
的实现是作为char
数组。 图11显示了32位Java运行时上StringBuffer
的内存使用情况和布局:
图11. 32位Java运行时上的StringBuffer
内存使用情况和布局
图11显示了创建StringBuffer
时,结果是一个使用24字节内存的StringBuffer
对象,以及一个默认大小为16的字符数组,一个空StringBuffer
总共有72字节数据。
像集合一样, StringBuffer
具有默认容量和调整大小的机制。 表7显示了StringBuffer
的属性:
表7. StringBuffer
属性
属性 | 描述 |
---|---|
默认容量 | 16 |
空尺寸 | 72字节 |
高架 | 24字节 |
10K收集的开销 | 24字节 |
搜索/插入/删除效果 | 不适用 |
收藏集中的空白空间
具有给定数量的对象的各种集合的开销并不是整个内存开销的故事。 前述示例中的测量结果假设集合的大小已正确确定。 但是对于大多数收藏来说,这不太可能是正确的。 大多数集合都是以给定的初始容量创建的,并将数据放入集合中。 这意味着集合的容量通常大于集合中存储的数据,这会带来额外的开销。
考虑一个StringBuffer
的示例。 它的默认容量是16个字符条目,大小为72个字节。 最初,没有数据存储在72个字节中。 如果将一些字符放入字符数组(例如"MY STRING"
,则将在16个字符的数组中存储9个字符。 图12显示了在32位Java运行时上包含"MY STRING"
的StringBuffer
的内存使用情况:
图12. 32位Java运行时上包含"MY STRING"
的StringBuffer
内存使用情况
如图12所示,未使用数组中的7个附加字符条目,但它们正在占用内存-在这种情况下,附加开销为112个字节。 对于此集合,您有9个条目,容量为16,这使您的填充率为0.56。 集合的填充率越低,由于备用容量导致的开销就越大。
扩展和调整集合大小
集合达到其容量并请求将其他条目放入集合后,将调整集合的大小并对其进行扩展以容纳新条目。 这样可以增加容量,但通常会降低填充率,并增加内存开销。
集合之间使用的扩展算法不同,但是一种常见的方法是使集合的容量增加一倍。 这是StringBuffer
采取的方法。 对于前面的示例的StringBuffer
,如果要在缓冲区中追加" OF TEXT"
以产生"MY STRING OF TEXT"
,则需要扩展集合,因为新的字符集合在当前容量下有17个条目之16。图13显示了产生的内存使用情况:
图13.在32位Java运行时上包含"MY STRING OF TEXT"
的StringBuffer
内存使用情况
现在, 如图13所示,您有一个32个条目的字符数组和17个已使用的条目,填充率为0.53。 填充率并没有显着下降,但是您现在有240字节的备用容量开销。
对于较小的字符串和集合,低填充率和备用容量的开销似乎不是太大的问题,但是在更大的尺寸下它们变得更加明显和昂贵。 例如,如果您创建一个仅包含16MB数据的StringBuffer
,(默认情况下)它将使用字符数组,该字符数组的大小可容纳32MB数据,从而以备用容量的形式增加16MB的额外开销。
Java集合:摘要
表8总结了集合的属性:
表8.集合属性摘要
采集 | 性能 | 默认容量 | 空尺寸 | 1万个入门开销 | 尺寸正确吗? | 扩展算法 |
---|---|---|---|---|---|---|
HashSet | O(1) | 16 | 144 | 360K | 没有 | 2倍 |
HashMap | O(1) | 16 | 128 | 360K | 没有 | 2倍 |
Hashtable | O(1) | 11 | 104 | 360K | 没有 | x2 + 1 |
LinkedList | 上) | 1个 | 48 | 24万 | 是 | +1 |
ArrayList | 上) | 10 | 88 | 40K | 没有 | x1.5 |
StringBuffer | O(1) | 16 | 72 | 24 | 没有 | 2倍 |
Hash
集合的性能比List
的任何一个都要好得多,但每个条目的成本要高得多。 由于访问性能的原因,如果要创建大型集合(例如,实现高速缓存),则最好使用基于Hash
的集合,而无需考虑额外的开销。
对于访问性能影响较小的较小集合,可以使用List
。 ArrayList
和LinkedList
集合的性能大致相同,但是它们的内存占用量有所不同: ArrayList
的每个条目大小比LinkedList
小得多,但大小不准确。 ArrayList
或LinkedList
是要使用的List
的正确实现方式,取决于List
长度的可预测性。 如果长度未知,则LinkedList
可能是正确的选择,因为该集合将包含较少的空白空间。 如果知道大小,则ArrayList
内存开销会少得多。
选择正确的收集类型使您能够在收集性能和内存占用之间选择适当的平衡。 此外,您可以通过正确调整集合的大小来最大程度地减少内存占用,以最大程度地提高填充率并最大程度地减少未使用的空间。
使用中的集合:PlantsByWebSphere和WebSphere Application Server版本7
在表8中 ,创建一个10,000个基于Hash
的集合的开销显示为360K。 鉴于复杂的Java应用程序与千兆字节大小的Java堆一起运行并不少见,因此这似乎并不会带来很大的开销-除非当然要使用大量的集合。
表9显示了WebSphere®Application Server版本7随附的PlantsByWebSphere示例应用程序在五用户负载测试下运行时,收集对象的使用作为206MB Java堆使用的一部分:
表9. PlantsByWebSphere在WebSphere Application Server v7上的集合用法
收集类型 | 实例数 | 收集总开销(MB) |
---|---|---|
Hashtable | 262,234 | 26.5 |
WeakHashMap | 19562 | 12.6 |
HashMap | 10,600 | 2.3 |
ArrayList | 9,530 | 0.3 |
HashSet | 1,551 | 1.0 |
Vector | 1,271 | 0.04 |
LinkedList | 1,148 | 0.1 |
TreeMap | 299 | 0.03 |
总 | 306,195 | 42.9 |
从表9中可以看到,正在使用300,000多个不同的集合,并且集合本身(不计算它们包含的数据)占206MB Java堆使用量的42.9MB(21%)。 这意味着,如果您更改集合类型或确保集合的大小更准确,则可以节省大量潜在的内存。
使用Memory Analyzer寻找低填充率
作为IBM Support Assistant的一部分提供的IBM Java监视和诊断工具-内存分析器工具(内存分析器)可以分析Java集合的内存使用情况(请参阅参考资料 )。 它的功能包括分析填充率和集合大小。 您可以使用此分析来确定可以进行优化的所有集合。
Memory Analyzer中的收集分析功能位于Open Query Browser-> Java Collections菜单下,如图14所示:
图14.在内存分析器中分析Java集合的填充率
在图14中选择的“集合填充率”查询对于识别比当前所需的要大得多的集合最有用。 您可以为此查询指定许多选项,包括:
- objects :您感兴趣的对象(集合)的类型
- segments :填充率范围可将对象分组
将object选项设置为“ java.util.Hashtable”并将segments选项设置为“ 10”来运行查询,将产生如图15所示的输出:
图15.在内存分析器中分析Hashtable
的填充率
图15显示了262,234个java.util.Hashtable
实例java.util.Hashtable
127,016个(48.4%)是完全空的,并且几乎所有实例都只有少量的条目。
然后可以通过选择结果表的一行并右键单击以选择列表对象->具有传入引用以查看哪些对象拥有这些集合,或右键单击以选择哪些对象拥有这些集合,或单击列表对象->具有传出引用以查看这些集合中的内容来标识这些集合。集合。 图16显示了查看空Hashtable
的传入引用并扩展几个条目的结果:
图16.在内存分析器中对空Hashtable
的传入引用的分析
图16显示了一些空的Hashtable
由javax.management.remote.rmi.NoCallStackClassLoader
代码拥有。
通过查看Memory Analyzer左侧面板中的Attributes视图,您可以看到有关Hashtable
本身的特定详细信息,如图17所示:
Figure 17. Inspection of the empty Hashtable
in Memory Analyzer
Figure 17 shows that the Hashtable
has a size of 11 (the default size) and that it is completely empty.
For the javax.management.remote.rmi.NoCallStackClassLoader
code, it might be possible to optimize the collection usage by:
- Lazily allocating the
Hashtable
: If it is common for theHashtable
to be empty, then it may make sense for theHashtable
to be allocated only when there is data to store inside it. - Allocating the
Hashtable
to an accurate size: Because the default size has been used, it's possible that a more accurate initial size could be used.
Whether either or both of these optimizations are applicable depends on how the code is commonly used, and what data is commonly stored inside it.
Empty collections in the PlantsByWebSphere example
Table 10 shows the result of analyzing collections in the PlantsByWebSphere example to identifying those that are empty:
Table 10. Empty-collection usage by PlantsByWebSphere on WebSphere Application Server v7
Collection type | Number of instances | Empty instances | % Empty |
---|---|---|---|
Hashtable | 262,234 | 127,016 | 48.4 |
WeakHashMap | 19562 | 19,465 | 99.5 |
HashMap | 10,600 | 7,599 | 71.7 |
ArrayList | 9,530 | 4,588 | 48.1 |
HashSet | 1,551 | 866 | 55.8 |
Vector | 1,271 | 622 | 48.9 |
Total | 304,748 | 160,156 | 52.6 |
Table 10 shows that on average, over 50 percent of the collections are empty, implying that significant memory-footprint savings could be gained by optimization of collection usage. It could be applied to various levels of the application: in the PlantsByWebSphere example code, in the WebSphere Application Server, and in the Java collections classes themselves.
Between WebSphere Application Server version 7 and version 8, some work has been done to improve memory efficiency in the Java collections and middleware layers. For example, a large percentage of the overhead of instances of java.util.WeahHashMap
is due to the fact that it contains an instance of java.lang.ref.ReferenceQueue
to handle the weak references. Figure 18 shows the memory layout of a WeakHashMap
for a 32-bit Java runtime:
Figure 18. Memory layout of a WeakHashMap
for a 32-bit Java runtime
Figure 18 shows that the ReferenceQueue
object is responsible for retaining 560 bytes' worth of data, even if the WeakHashMap
is empty and ReferenceQueue
is therefore not required. For the PlantsByWebSphere example case with 19,465 empty WeakHashMap
s, the ReferenceQueue
objects are adding an additional 10.9MB of data that is not required. In WebSphere Application Server version 8 and the Java 7 release of the IBM Java runtimes, the WeakHashMap
has undergone some optimization: It contains a ReferenceQueue
, which in turn contains an array of Reference
objects. That array has been changed to be allocated lazily — that is, only when objects are added to the ReferenceQueue
.
结论
A large and perhaps surprising number of collections exist in any given application, and more so for complex applications. Use of a large number of collections often provides scope for achieving sometimes significant memory-footprint savings by selecting the right collection, sizing it correctly, and potentially by allocating it lazily. These decisions are best made during design and development, but you can also use the Memory Analyzer tool to analyze your existing applications for potential memory-footprint optimization.
翻译自: https://www.ibm.com/developerworks/java/library/j-codetoheap/index.html