什么是垃圾收集
垃圾收集器(GC)会自动管理应用程序的动态内存分配请求。
垃圾收集器通过以下操作执行自动动态内存管理:
- 从操作系统分配和归还内存。
- 根据应用程序的请求将内存分发给应用程序。
- 确定哪些内存部分仍被应用程序使用。
- 回收未使用的内存供应用程序再次使用。
Java HotSpot垃圾收集器采用各种技术来提高这些操作的效率:
- 结合使用分代清除和老化技术,将重点集中在堆中可能包含大量可回收内存区域的区域。
- 使用多个线程积极地使操作并行,或在后台执行一些长时间运行的操作,与应用程序并发执行。
- 通过压缩活动对象来尝试恢复更大的连续空闲内存。
为什么选择合适的垃圾收集器那么重要
垃圾收集器的目的是将应用程序开发人员从手动动态内存管理中解放出来。开发人员无需再将分配与释放相匹配,也无需密切关注已分配动态内存的生存周期。这完全消除了与内存管理相关的一些错误类别,但代价是增加了一些额外的运行时开销(比如cpu和内存)。Java HotSpot VM提供了一系列可供选择的垃圾收集算法。
选择垃圾收集器很重要?对于某些应用程序来说,答案是从不重要。对一些小型应用,应用程序在存在垃圾收集的情况下,也能够很好地运行,即使有适度频率和持续时间的暂停。然而,这对于其他应用程序来说并非如此,尤其是那些拥有大量数据(1G以上内存空间)、多线程和高事务率的应用程序。
阿姆达尔定律(给定问题中的并行加速受限于问题的顺序部分)表明,大多数工作负载无法完全并行化;总有一部分是顺序执行的,无法从并行化中受益。在Java平台上,目前支持四种垃圾收集替代方案,除了串行GC之外,其他所有方案都通过并行化工作来提高性能。尽可能降低垃圾收集的开销非常重要。这可以从以下示例中看出。
图1-1中的图表展示了一个理想系统,该系统除了垃圾收集之外完全可以扩展。红线表示在一个单处理器系统中,应用程序仅将1%的时间花在垃圾收集上。这意味着在拥有32个任务的系统上,吞吐量损失超过20%。紫红色线显示,如果一个应用程序在垃圾收集上花费了10%的时间(在单处理器应用程序中,这并不算是一个过长的时间),那么在扩展到32个任务时,吞吐量损失将超过75%。
该图表表明,在开发小型系统时可能可以忽略的吞吐量问题,在扩展到大型系统时可能会成为主要的瓶颈。然而,在减少此类瓶颈方面取得的小幅改进可以带来性能上的大幅提升。对于足够大的系统来说,选择正确的垃圾收集器并在必要时进行调整变得非常有价值。
串行收集器通常足以满足大多数小型应用程序的需求,尤其是那些在现代处理器上需要最多约100兆字节堆的应用程序。其他收集器具有额外的开销或复杂性,这是为了获得特定行为而付出的代价。如果应用程序不需要替代收集器的特定行为,请使用串行收集器。串行收集器可能不是最佳选择的情况之一是,在具有大量内存和两个或更多处理器的机器上运行的大型、多线程应用程序。当应用程序在此类服务器级机器上运行时,默认情况下会选择Garbage-First(G1)收集器;
此外,在选择垃圾收集器时,还需要考虑其他因素,如应用程序的内存需求、CPU使用情况、响应时间等。这些因素都可能影响垃圾收集器的性能和效率。因此,在实际应用中,开发者需要根据具体的应用场景和需求,选择最适合的垃圾收集器,并进行相应的配置和调优,以达到最佳的性能表现。
需要注意的是,垃圾收集器的选择并不是一蹴而就的,可能需要根据应用程序的运行情况进行多次尝试和调整。同时,随着Java技术的不断发展,新的垃圾收集算法和工具也在不断涌现,开发者需要保持对新技术的学习和关注,以便更好地应对各种复杂的内存管理问题。
JVM的默认配置
JVM 为垃圾收集器、堆大小和运行时编译器提供与平台相关的默认选择。这些选择匹配不同类型应用程序的需求,同时需要较少的命令行调优。此外,基于行为的调优会动态优化堆的大小,以满足应用程序的指定行为。
垃圾收集器、堆和运行时编译器的默认选择
以下是重要的垃圾收集器、堆大小和运行时编译器的默认选择:
- 在服务器级机器上默认使用Garbage-First(G1)收集器,其他情况下则使用串行收集器。
- GC线程的最大数量受到堆大小和可用CPU资源的限制。
- 初始堆大小为物理内存的1/64。
- 最大堆大小为物理内存的1/4。
- 分层编译器(Tiered compiler),同时使用C1和C2。
在Java虚拟机中,这些默认值旨在在各种应用场景下提供合理的性能。G1收集器被设计为能够在大型堆上提供高吞吐量和可预测的暂停时间,这对于服务器级应用来说至关重要。而在小型应用或资源受限的环境中,串行收集器由于其简单性和较低的开销而更为适用。
堆大小的默认设置旨在平衡内存使用与性能。太小的堆可能导致频繁的垃圾收集,从而影响性能;而太大的堆则可能浪费内存资源。通过根据物理内存自动调整堆大小,Java虚拟机试图找到一个平衡点。
分层编译器是Java HotSpot虚拟机中的一种编译策略,它结合了C1编译器的快速启动优势和C2编译器的优化性能。在程序运行初期,主要使用C1编译器进行快速编译,以加快程序启动速度;随着程序运行时间的增长,C2编译器会逐渐接手,对热点代码进行更深入的优化,以提高运行效率。
需要注意的是,这些默认值并非一成不变,它们可能会随着Java虚拟机版本的更新而发生变化。此外,开发者还可以根据应用的具体需求,通过JVM参数来手动调整这些设置,以达到最佳的性能表现。例如,可以通过设置-Xms
和-Xmx
参数来手动指定堆的初始大小和最大大小,或者使用-XX:+UseG1GC
来启用G1收集器等。因此,开发者需要保持对Java虚拟机技术和相关参数的了解,以便能够根据实际情况进行灵活调整。
注意: 如果 VM 检测到两个以上的处理器和大于或等于1792 MB 的堆,则 VM 将机器视为服务器类。
基于行为的调优
Java HotSpot VM的垃圾收集器可以配置为优先满足两个目标之一:最大暂停时间和应用程序吞吐量。如果首选目标得到满足,收集器将尝试最大化另一个目标。然而,这些目标并非总是可以同时满足:应用程序需要最小堆来至少保存所有活动数据,而其他配置可能会阻碍达到某些或所有期望的目标。
最大暂停时间目标
暂停时间是指垃圾收集器停止应用程序并回收不再使用的空间所持续的时间。最大暂停时间目标的意图是限制这些暂停中的最长暂停时间。
垃圾收集器会维护一个关于暂停的平均时间和该平均时间的方差。这个平均值是从程序执行开始时计算的,但它是加权的,以便最近的暂停对平均值的影响更大。如果暂停时间的平均值加上方差大于最大暂停时间目标,那么垃圾收集器就认为目标未得到满足。
最大暂停时间目标通过命令行选项 -XX:MaxGCPauseMillis=<nnn>
来指定。这被解释为对垃圾收集器的一个提示,即期望的暂停时间应小于或等于 <nnn>
毫秒。垃圾收集器会调整Java堆的大小和与垃圾收集相关的其他参数,以尝试保持垃圾收集的暂停时间小于 <nnn>
毫秒。最大暂停时间目标的默认值因收集器而异。这些调整可能会导致垃圾收集更频繁地发生,从而降低应用程序的整体吞吐量。但在某些情况下,可能无法达到期望的暂停时间目标。
吞吐量目标
吞吐量目标是根据垃圾收集所花费的时间来衡量的,而垃圾收集之外的时间则是应用程序的运行时间。
该目标通过命令行选项 -XX:GCTimeRatio=nnn 来指定。垃圾收集时间与应用程序时间的比率是 1/(1+nnn)。例如,-XX:GCTimeRatio=19 设置了一个目标,即垃圾收集时间占总时间的 1/20 或 5%。
垃圾收集所花费的时间是所有垃圾收集导致的暂停的总时间。如果吞吐量目标未能达到,垃圾收集器可能采取的一个行动是增加堆的大小,以便在收集暂停之间应用程序的运行时间可以更长。
占用空间
在吞吐量和最大暂停时间目标得到满足后,垃圾收集器会尝试最小化堆的大小,逐步减少它,直到无法再满足其中一个目标(通常是吞吐量目标)。可以使用 -Xms=<nnn> 和 -Xmx=<mmm> 选项分别设置堆的最小和最大大小,以指定期望的堆的下限和上限。
调优策略
堆的大小会根据所选的吞吐量目标进行增长或缩小。了解堆调优策略,如选择最大堆大小,以及选择最大暂停时间目标。
除非你确定需要比默认最大堆大小更大的堆,否则不要为堆选择最大值。为你的应用程序选择一个足够的吞吐量目标。
应用程序行为的改变可能会导致堆的大小增长或缩小。例如,如果应用程序开始以更高的速率进行分配,那么堆会增长以维持相同的吞吐量。
如果堆增长到其最大大小且吞吐量目标仍未达到,那么对于吞吐量目标来说,最大堆大小太小。将最大堆大小设置为接近平台上的总物理内存的值,但不会导致应用程序交换。再次执行应用程序。如果吞吐量目标仍未达到,那么对于平台上的可用内存来说,应用程序时间的目标过高。
如果吞吐量目标可以达到,但暂停时间过长,那么选择一个最大暂停时间目标。选择最大暂停时间目标可能意味着吞吐量目标无法达到,因此请为应用程序选择可接受的折衷值。
垃圾收集器试图满足相互竞争的目标时,堆的大小通常会振荡,即使应用程序已达到稳定状态也是如此。实现吞吐量目标(可能需要更大的堆)的压力与实现最大暂停时间和最小占用空间目标(两者都可能需要较小的堆)的压力相互竞争。