首先,请注意,JDK集合的内部并不是魔术。它们是用Java编写的。他们的源代码随JDK一起提供,因此您可以在IDE中打开它。也可以在网上轻松找到。而且,事实证明,在优化内存占用方面,大多数集合都不是很完善。
例如,考虑以下最简单和最受欢迎的集合类之一:java.util.ArrayList。内部每个ArrayList维护一个Object[] elementData数组。这就是列表元素的存储位置。让我们看看如何管理这个数组。
当您ArrayList使用默认构造函数创建一个时,即invoke new ArrayList(), elementData 被设置为指向单例共享的零大小数组(elementData也可以设置为null,但是单例数组提供了一些次要的实现优势)。将第一个元素添加到列表后,将elementData 创建一个真实的唯一数组,并将所提供的对象插入其中。为避免每次添加新元素时都调整数组大小,将以长度10(“默认容量”)创建该元素。这里有一个陷阱:如果您从不向此添加更多元素ArrayList,则elementData 阵列中10个插槽中的9个将保持为空。即使以后清除此列表,内部阵列也不会缩小。下图总结了这个生命周期:
JDK集合是列表和地图的标准库实现。如果你看看一个典型的大的Java应用程序的内存快照,你会看到成千上万甚至数百万的实例java.util.ArrayList,java.util.HashMap等集合是在内存中的数据存储和操作是必不可少的。但是,您是否考虑过应用程序中的所有集合是否都以最佳方式使用内存?换句话说:如果您的Java应用程序因臭名昭著的OutOfMemoryErrorGC 崩溃或经历了长时间的GC暂停而崩溃了,您是否检查了其收集的内存浪费?如果您的回答是“否”或“不确定”,请继续阅读。
这里浪费了多少内存?绝对而言,它的计算方式是(对象指针大小)*9。如果使用HotSpot JVM(Oracle JDK随附),则指针大小取决于最大堆大小(请参阅https://blog.codecentric.de/ zh-CN / 2014/02 / 35gb-heap-less-32gb-java-jvm-memory-oddities / )。通常,如果您指定-Xmx小于32 GB,则指针大小为4个字节;对于较大的堆,它是8个字节。因此,ArrayList使用默认构造函数进行初始化(仅添加一个元素)会浪费36或72个字节。
实际上,一个空也 ArrayList浪费了内存,因为它不承担任何工作量,但是ArrayList对象本身的大小非零且比您想象的要大。这是因为,一方面,由HotSpot JVM管理的每个对象都有一个12字节或16字节的标头,JVM将该标头用于内部目的。接下来,大多数集合对象都包含该size字段,指向内部数组或另一个“工作负载载体”对象的指针,modCount用于跟踪内容修改的字段等。因此,即使是表示空集合的最小对象也可能至少需要32个对象。内存字节。有些,例如ConcurrentHashMap,需要更多。
考虑另一个无处不在的集合类:java.util.HashMap。其生命周期与相似, ArrayList总结如下:
如您所见,HashMap 仅包含一个键/值对的浪费15个内部数组插槽,这将转换为60或120个字节。这些数字很小,但是重要的是,以您的应用程序相对而言,所有集合都会损失多少内存。事实证明,某些应用程序会以这种方式浪费很多时间。例如,作者分析了几个流行的开源Hadoop组件,在某些情况下损失了大约20%的堆!对于由经验不足的工程师开发且未定期检查其性能的产品,内存浪费可能更高。有足够的用例,例如,一棵大树中90%的节点仅包含一个或两个子代(或根本不包含任何子代),以及其他情况下堆中充满了0、1、2元素集合。
如果您在应用中发现未使用或未充分利用的集合,该如何解决?以下是一些常见的食谱。在这里,我们有问题的集合被假定为数据字段的ArrayList引用Foo.list。
如果list 从未使用过大多数实例 ,请考虑将其延迟初始化。因此,以前看起来像的代码...
void addToList(Object x)
{
list.add(x);
}
void addToList(Object x)
{
getOrCreateList().add(x);
}
private list getOrCreateList()
{
// To conserve memory, we don't create the list until the first use
if
(
list
== null)
list = new ArrayList();
return list ;
}
请记住,有时您需要采取其他措施来解决可能的比赛。例如,如果您维护一个ConcurrentHashMap可以同时由多个线程更新的,则延迟对其进行初始化的代码不应允许两个线程意外地创建此映射的两个副本:
private Map getOrCreateMap()
{
if (map == null) {
// Make sure we aren't outpaced by another thread
synchronized (this) {
if (map== null)
map=new ConcurrentHashMap();
}
}
return map ;
}
如果list或map的大多数实例仅包含少量元素,请考虑使用更合适的初始容量进行初始化,例如
list = new ArrayList(4);
// Internal array will start with length 4
如果在大多数情况下您的集合为空或仅包含一个元素(或键值对),则可以考虑一种极端的优化形式。仅当在给定的类中对集合进行完全管理时才有效,即其他代码无法直接访问它。想法是将数据字段的类型从例如 List更改为更通用的Object,以便它现在可以指向真实列表,也可以直接指向唯一的列表元素。这是一个简短的草图:
// *** Old code ***
private List<Foo> list = new ArrayList<>();
void addToList(Foo foo)
{
list.add(foo); }
// *** New code ***
// If list is empty, this is null. If list contains only one element,
// this points directly to that element. Otherwise, it points to a
// real ArrayList object.
private Object listOrSingleEl;
void addToList(Foo foo) {
if (listOrSingleEl == null) {
// Empty list
listOrSingleEl = foo;
}
else
if (listOrSingleEl instanceof Foo) {
// Single-element
Foo firstEl = (Foo) listOrSingleEl;
ArrayList<Foo> list = new ArrayList<>();
listOrSingleEl = list ;
list.add(firstEl);
list.add(foo);
}
else{
// Real, multiple-element list
((ArrayList<Foo>) listOrSingleEl).add(foo);
}
}
显然,这种优化使您的代码可读性降低,维护起来也更加困难。但是,如果您知道您将通过这种方式节省大量内存,或者摆脱了长时间的GC暂停,那可能是值得的。
这可能已经引起您的思考:我如何知道应用程序中的哪些集合浪费了内存,有多少?
简短的答案是:如果没有适当的工具,这很难发现。试图猜测大型复杂应用程序中数据结构使用或浪费的内存量几乎是行不通的。而且,在不确切知道内存在哪里的情况下,您可能会花费大量时间追踪错误的目标,而应用程序却顽固地失败了OutOfMemoryError。
因此,您需要使用工具检查应用程序的堆。根据经验,分析JVM内存(以可用信息量与工具对应用程序性能的影响来衡量)的最佳方法是获取堆转储,然后脱机查看它。堆转储本质上是堆的完整快照。可以在任意时刻通过调用jmap实用程序来获取它,或者可以将JVM配置为在失败时自动生成它OutOfMemoryError。如果您使用Google进行“ JVM堆转储”,您将立即看到一堆文章,详细解释了如何获得转储。
堆转储是大约JVM堆大小的二进制文件,因此只能使用特殊工具读取和分析。有许多这样的工具,包括开源和商业工具。最受欢迎的开源工具是Eclipse MAT;还有VisualVM和一些功能较弱且鲜为人知的工具。商业工具包括通用Java探查器:JProfiler和YourKit,以及专门为堆转储分析而构建的一种工具,称为JXRay(免责声明:作者开发了后者)。
与其他工具不同,JXRay会立即分析堆转储,以解决大量常见问题,例如重复的字符串和其他对象以及次优的数据结构。上述集合的问题属于后一类。该工具将生成一个报告,其中包含所有收集的HTML格式信息。这种方法的优点是您可以随时随地查看分析结果,并轻松与他人共享。这也意味着您可以在任何计算机上运行该工具,包括数据中心中功能强大但“无头”的计算机。
JXRay以字节为单位,并以已用堆的百分比来计算开销(如果解决特定问题,将节省多少内存)。它将具有相同问题的同一类的集合归为一类。
...然后将可从某个GC根目录通过同一参考链访问的有问题的集合分组,如下例所示
知道哪些引用链和/或单个数据字段(例如,INodeDirectory.children上面的数据)指向浪费大多数内存的集合,可以使您快速而精确地找出导致问题的代码,然后进行必要的更改。
总之,配置欠佳的Java集合可能会浪费大量内存。在许多情况下,此问题很容易解决,但是有时,您可能需要以非平凡的方式更改代码以取得重大改进。很难猜测需要优化哪些集合以产生最大的影响。为了避免浪费时间优化代码的错误部分,您需要获取JVM堆转储并使用适当的工具对其进行分析。
如果您喜欢本文,并且想了解有关Java集合的更多信息,请查看有关Java 集合的 所有东西的教程和文章的集合以及关注我们!
网友评论