当前位置:Java -> Java集合开销
Java虚拟机在优化性能的同时实现了Java应用的跨平台特性。在考虑性能,尤其是内存利用方面,理解Java集合框架特别是ArrayList
处理大小和容量的方式至关重要。
本文将着重探讨包含两到三个元素的列表所带来的额外开销。这是因为这种情况更为常见,很容易被忽视。
列表的大小指的是当前存储在其中的元素数量。 随着添加或删除元素,它可能会发生变化。方法List.size()
提供了这个数量。如果我们有一个包含十个项目的列表,它的大小就是十。
列表的容量是指为存储元素而分配的内存量,不管这些内存位置当前是否在使用。容量主要是与由数组支持的列表有关,比如ArrayList
。 容量表示列表在调整其内部存储数组大小之前可以容纳的最大元素数量。 容量始终大于或等于列表的大小。
如果我们初始化一个ArrayList
并向其添加十个项目,则其大小是十。但是,底层数组可能有容量可以容纳十五个项目。 这就意味着向列表添加五个以上的项目不会触发底层数组的扩展。
理解大小和容量之间的区别至关重要。 尺寸确定了实际数据计数,容量影响了内存利用,并可能影响性能,因为可能需要数组调整大小和数据复制。
ArrayList
类具有默认的初始容量。截至Java 17,这个容量是十。如果我们知道将有更多或更少的元素,通常最好设置初始容量以减少调整大小的次数。
例如,LinkedList
没有容量的概念。它是一个双向链表,意味着每个元素都指向其前驱和后继。这里没有需要调整大小的底层数组。
在考虑JVM性能时,了解列表的初始容量以及它们如何增长至关重要。设置适当的初始容量可以减少列表调整大小的需求,减少内存使用并提高性能。
让我们进行两个测试来比较代码的性能:
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1, jvmArgs = {"-Xlog:gc:file=list-creation-%t.txt,filecount=10,filesize=40gb -Xmx6gb
-Xms6gb"})
public void listCreationBenchmark(HeapDumperState heapDumperState, Blackhole blackhole) {
final List<Integer> result = new ArrayList<>();
result.add(1);
result.add(2);
result.add(3);
blackhole.consume(result);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 2, jvmArgs = {"-Xlog:gc:file=limited-capacity-list-creation-
%t.txt,filecount=10,filesize=40gb -Xmx6gb -Xms6gb"})
public void limitedCapacityListCreationBenchmark(HeapDumperState heapDumperState, Blackhole blackhole) {
final List<Integer> result = new ArrayList<>(3);
result.add(1);
result.add(2);
result.add(3);
blackhole.consume(result);
}
请注意,HeapDumperState
是一个状态,每次迭代后都会触发对堆的转储,以便获取有关创建对象的信息。所有测试在两个独立的分支中运行了十个十分钟迭代。每个测试的总持续时间约为一个小时四十分钟。
总的来说,在这些测试中并没有显示出显著的差异,并且据偶然情况来看,所有运行结果实际上显示默认容量的第一个选项可能更快:
基准 | 模式 | Cnt | 得分 | 误差 | 单位 |
---|---|---|---|---|---|
OverheadBenchmark.limitedCapacityListCreationBenchmark | thrpt | 20 | 116365294.187 | ± 4748264.227 | ops/s |
OverheadBenchmark.listCreationBenchmark | thrpt | 20 | 121014905.085 | ± 188451.671 | ops/s |
单个操作的平均时间如此微小,以至于在这些测试中很难看出差异。请记住,我们在测量创建一个具有三个元素的ArrayList
与十个元素的列表。
在优化性能时,了解内存分配决策对JVM的影响非常重要。我们可能会认为ArrayList
的默认容量是无害的。然而,在某些情况下,这不仅对大型列表或高频列表操作有影响,对于元素数量较少的列表也有影响。
让我们深入研究这些测试产生的垃圾收集日志。我们将使用HeapHero进行分析。最初的猜测可能是,具有默认容量的ArrayList
测试会占用更多的堆空间,有更多的垃圾收集周期,并降低吞吐量。
当使用默认构造函数初始化ArrayList(即,未指定容量时),列表将为十个元素分配内存。如果我们仅添加三个元素,则使用了所分配内存的30%,留下了70%未使用。
如果我们使用容量为三的ArrayList
进行初始化(new ArrayList<>(3)
),它将只为这三个元素分配内存。因此,浪费较少。
这一点在平均堆大小的差异中清晰地表现出来。具有显式声明容量的ArrayList
提供了以下结果:
同时,正如我们预期的那样,具有默认容量的ArrayList
结果消耗的内存更多:
由于内存占用,JVM必须更积极地管理内存,进行更频繁的垃圾回收周期。让我们比较一下这两种情况的关键绩效指标:
ArrayLists
测试中明显更高。
请考虑,对象几乎瞬间就会变得不可达。然而,在繁忙的网络服务器上,这可能会引发更多问题。
尽管之前的指标确定了问题,但它们没有确定问题的来源。为了找出问题的根源,我们可以使用堆转储分析来了解堆的状态。特别是,我们将集中在集合效率低下部分。
该部分提供了对由于过大集合而造成的内存浪费的概要。这个问题的主要原因是集合的容量和大小之间的差异。
堆转储是在进行垃圾回收周期之前捕获的。这样,我们可以更好地看到我们堆中的集合对象:
图 5: yCrash中的低效集合信息
从这里我们可以看出,我们浪费了内存,并且我们的大多数集合(几乎所有)占用的空间比它们实际所需的多。
对应用进行持续监视和分析对其健康和高性能运行至关重要。有时,偶尔的堆转储和垃圾回收日志难以发现问题。这就是为什么拥有一个能够持续分析应用程序的系统至关重要。yCrash应用程序可以帮助监控,并不仅可以提供更好的用户体验,还可以使服务在市场上获得竞争优势。
推荐阅读: 银行等金融行业春招信息汇总(更新)
本文链接: Java集合开销