参考:https://docs.oracle.com/javase/10/gctuning/
目录(不只是搬运工...)
6.4、并行收集器时间过长、OutOfMemoryError
1、垃圾收集器简介
1.1、什么是垃圾收集器?
垃圾收集器(GC)自动管理应用程序的动态内存分配请求。
垃圾收集器通过以下操作自动动态管理内存:
- 从操作系统中申请内存、释放内存
- 给应用程序分配内存
- 确定应用程序仍在使用哪些部分的内存
- 收集未使用的内存供应用程序使用
Java HotSpot GC采用各种技术提高以上操作效率:
- 将分代清理与衰老机制结合,可以集中精力在堆中最可能包含大量垃圾收集内存的区域。
- 使用多线程并行操作,或者在后台与应用程序并发执行一些长时间运行的操作
- 尝试通过压缩活动对象来回复更大的连续可用内存
1.2、选择垃圾收集器的重要性?
Amdahl定律(给定问题的并行加速受到问题的连续部分的限制)意味着大多数工作负载无法完美并行化。某些部分总是顺序的,不会受益于并行性。保持垃圾收集的开销尽可能低是非常重要的。
该图显示,在小型系统上进行开发时,可忽略的吞吐量问题可能成为扩展到大型系统时的主要瓶颈。然而,在减少这种瓶颈方面的微小改进可以在性能上产生很大的提高。对于足够大的系统,选择正确的垃圾收集器并在必要时进行调整是值得的。
串行收集器通常适用于大多数小型应用程序,特别是那些在现代处理器上需要高达约100兆字节堆的应用程序。其他收集器具有额外的开销或复杂性,这是专门行为的代价。如果应用程序不需要备用收集器的特殊行为,请使用串行收集器。
串行收集器通常不适用于具有大量内存和两个或更多处理器的机器上运行的大型,高度线程化的应用程序的情况。当应用程序在此类服务器级计算机上运行时,默认情况下会选择Garbage-First(G1)收集器。
2、Ergonomics(人体工程学)
人体工程学是Java虚拟机(JVM)和垃圾收集启发式(例如基于行为的启发式)提高应用程序性能的过程。
JVM为垃圾收集器,堆大小和运行时编译器提供与平台相关的默认选择。 这些选择符合不同类型应用程序的需求,同时需要较少的命令行调整。 此外,基于行为的调优动态地优化堆的大小以满足应用程序的指定行为。
2.1、垃圾收集器、堆、编译器的默认选择
被称为服务器级机器的一类机器被定义为具有以下内容的机器:
- 两个或更多物理处理器
- 两个或更多GB的物理内存
在服务器级计算机上,默认情况下选择以下内容:
- Garbage-First(G1)收集器
- 初始堆大小为物理内存的1/64
- 最大堆大小为物理内存的1/4
- 分层编译器,使用C1和C2
2.2、基于行为的调整
可以将Java HotSpot VM 垃圾收集器配置为优先满足两个目标之一:最大暂停时间和应用程序吞吐量。如果满足首选目标,收藏家将尝试最大化另一个目标。当然,这些目标并不总能得到满足:应用程序需要最少的堆来保存至少所有实时数据,而其他配置可能会妨碍达到部分或全部预期目标。
-
最大暂停时间目标
暂停时间是垃圾收集器停止应用程序并恢复不再使用的空间的持续时间。最大暂停时间目标在于限制这些暂停的最长时间。
垃圾收集器维持平均停顿时间和平均方差。平均值从执行开始时计算,但加权使得最近的暂停计数占比更大。如果平均值加上暂停时间的方差大于最大暂停时间目标,则垃圾收集器认为该目标未满足。
使用命令行选型: -XX:MaxGCPauseMillis=
<nnn>指定最大暂停时间目标。垃圾收集器调整Java堆大小和垃圾收集相关的其他参数,试图使垃圾手机暂停时间小于<nnn>毫秒。最大暂停时间目标的默认值因收集器而异。这些调整可能导致垃圾收集更频繁地发生,从而降低了应用程序的整体吞吐量。在某些情况下,无法满足所需的暂停时间目标。
-
吞吐量目标
吞吐量目标是根据收集垃圾所花费的时间和垃圾收集之外的应用时间比率来衡量的。
吞吐量目标由命令行选项-XX:GCTimeRatio=
nnn指定。垃圾收集时间与应用时间的比率为1/(1+nnn)。如, -XX:GCTimeRatio=19
设置目标为垃圾总收集时间的1/20。
垃圾收集所花费的时间是所有垃圾收集引起的暂停的总时间。如果未满足吞吐量目标,则垃圾收集器的一个可能操作是增加堆的大小,以便在收集暂停之间在应用程序中花费的时间可以更长。
-
占用空间
如果已满足吞吐量和最大暂停时间目标,则垃圾收集器会减小堆的大小,直到无法满足其中一个目标(总是吞吐量目标)。 -Xms=
<nnn> 和-Xmx=
<mmm> 设置垃圾收集器的最小和最大堆大小。
2.3、调优策略
增大或缩小堆来支持吞吐量目标。了解堆调整策略,例如选择最大堆大小以及选择最大暂停时间目标。
除非知道需要的堆大于默认的最大堆大小,否则不要为堆选择最大值。选择足以满足应用的吞吐量目标。
应用程序行为的更改可能导致堆增长或缩小。例如,如果应用程序开始以更高的速率分配,则堆增长以保持相同的吞吐量。
如果堆增长到其最大大小并且未满足吞吐量目标,则最大堆大小对于吞吐量目标而言太小。将最大堆大小设置为接近平台上的总物理内存的值,但不会导致应用程序的交换。再次执行应用程序。如果仍未满足吞吐量目标,则应用程序时间的目标对于平台上的可用内存来说太高。
如果可以满足吞吐量目标,但暂停时间过长,则选择最大暂停时间目标。选择最大暂停时间目标可能意味着吞吐量目标将无法满足,因此请选择对应用程序而言可接受的折衷的值。
当垃圾收集器试图满足竞争目标时,堆的大小通常会振荡。即使应用程序已达到稳定状态,也是如此。实现吞吐量目标(可能需要更大的堆)的压力与目标竞争最大暂停时间和最小占用空间(两者都可能需要小堆)。
3、垃圾收集器实现
Java SE的一个优势是使开发人员免受内存分配和垃圾收集的复杂性的影响。
3.1、分代垃圾收集
当运行程序中任何活动对象都无法访问到某对象时(可达性分析),该对象被视为垃圾对象,其内存可以被VM收集。
理论上,最简单的垃圾收集算法每次运行时都会遍历每个可到达的对象。任何剩余的对象都被认为是垃圾。这种方法所花费的时间与活动对象的数量成正比,这对于维护大量实时数据的大型应用程序来说是不可行的。
Figure 3-1 对象寿命的典型分布
图3-1中的蓝色区域是对象寿命的典型分布。 x轴显示以分配的字节为单位测量的对象生存期。 y轴上的字节数是具有相应生命周期的对象中的总字节数。 左边的尖峰表示在分配后不久可以收集的物体(换句话说,已经“死亡”)。 例如,迭代器对象通常仅在单个循环的持续时间内处于活动状态。
3.2、分代
为优化上述情况,内存通过分代管理(内存中包含不同年龄的对象)。当每一代填满时,都会发生垃圾收集。
绝大大多对象都分配在年轻代中,并且大多数对象都在年轻代死亡。当年轻代填满时,会发生young gc,收集年轻代中死亡对象。各代垃圾收集所消耗的时间与活动对象的数量成正比。通常,在每次young gc的时候一些存活的对象会移动到老年代。最终,老年代被填满,从而产生major gc(同时触发young gc),收集整个堆。因为相关对象明显在增加,major gc比young gc消耗时间长。
Figure 3-2 串行收集器中分代的默认排列
在启动时,Java HotSpot VM会将整个Java堆保留在地址空间中,但除非需要,否则不会为其分配任何物理内存。覆盖Java堆的整个地址空间在逻辑上分为年轻代和老年代。为对象存储器保留的完整地址空间可以分为年轻代和老年代。
年轻代由eden和两个survivor(from、to)空间组成。大多数对象最初都是在eden中分配的。一个survivor空间在任何时候都是空的,并且在垃圾收集期间充当eden和其他survivor空间中的活物体的目的地。垃圾收集后,eden和survivor空间都是空的。在下一个垃圾收集中,交换两个survivor空间。最近填充的一个survivor空间中的活动对象复制到另一个survivor空间。以这种方式在survivor空间之间复制对象,直到它们被复制了一定次数或者没有足够的空间存在。这些对象将复制老年代中。此过程也称为老化。
3.3、性能注意事项
- 吞吐量:吞吐量是应用时间占总时间(垃圾收集时间+应用时间)的百分比。吞吐量包括分配内存所花费的时间(但通常不需要调整分配速度)
- 延迟:应用程序的响应性能。垃圾收集暂停时间会影响应用程序的响应能力。
用户对垃圾收集有不同的要求。例如,有些人认为Web服务器的正确度量标准是吞吐量,因为垃圾收集期间的暂停可能是可以容忍的,或者只是被网络延迟所掩盖。但是,在交互式图形程序中,即使短暂停顿也可能对用户体验产生负面影响。
一些用户对其他考虑因素很敏感。占用空间(Footprint)是一个过程的工作集,以页面和缓存行来衡量。在具有有限物理内存或许多进程的系统上,占用空间可能会影响可伸缩性。提示性(Promptness)是指对象变为死亡和内存可用之间的时间,这是分布式系统的一个重要考虑因素,包括远程方法调用(RMI)。
通常,选择特定代的尺寸是这些考虑因素之间的权衡。 例如,非常大的年轻代可以最大化吞吐量,但这样做是以占用空间,及时性和暂停时间为代价的。 年轻代的停顿可以通过使用小型年轻代来减少吞吐量。 一代的大小不会影响另一代的收集频率和暂停时间。
没有一种正确的方法可以选择分代的规模。 最佳选择取决于应用程序使用内存的方式以及用户要求。 因此,虚拟机对垃圾收集器的选择并不总是最佳的,可以使用命令行选项覆盖, 查看影响垃圾收集性能的因素。
3.4、吞吐量和占用空间测量
使用特定于应用程序的度量来最好地测量吞吐量和占用空间。
例如,可以使用客户端负载生成器测试Web服务器的吞吐量,而可以使用 pmap
命令在Solaris操作系统上测量服务器的占用空间。 但是,通过检查虚拟机本身的诊断输出,可以轻松估计由于垃圾收集导致的暂停。
命令行选项-verbose:gc
打印有关每个集合的堆和垃圾收集的信息。 这是一个例子:
[15,651s][info ][gc] GC(36) Pause Young (G1 Evacuation Pause) 239M->57M(307M) (15,646s, 15,651s) 5,048ms
[16,162s][info ][gc] GC(37) Pause Young (G1 Evacuation Pause) 238M->57M(307M) (16,146s, 16,162s) 16,565ms
[16,367s][info ][gc] GC(38) Pause Full (System.gc()) 69M->31M(104M) (16,202s, 16,367s) 164,581ms
以上输出显示了两次young gc和一次由 System.gc()
触发的full gc。行开头的时间戳[15,651s]表示应用程序启动后的时间。然后记录日志级别[info]和标记信息[gc]。然后是GC识别号(36)。然后是GC类型(Pause Young、Pause Full)和GC原因(G1 Evacuation Pause)。然后是内存消耗信息。该日志记录 "GC前" -> "GC后" ("堆大小")。
在该示例的第一行中,239M-> 57M(307M),意味着在GC之前使用了239 MB并且GC清除了大部分内存,但是57 MB存活了。 堆大小为307 MB。 请注意,在此示例中,完整的GC将堆从307 MB缩小到104 MB。 在内存使用信息之后,将记录GC的开始和结束时间以及持续时间(结束 - 开始)。
-verbose:gc
命令是 -Xlog:gc
的别名。-Xlog
是用于登录HotSpot JVM常规日志记录配置选项。 是一个基于标签的系统,gc
就是一个标签。 要获取有关GC正在执行的操作的更多信息,可以配置日志记录以打印具有gc
标记和任何其他标记的任何消息。使用 -Xlog:gc*
[10.178s][info][gc,start ] GC(36) Pause Young (G1 Evacuation Pause)
[10.178s][info][gc,task ] GC(36) Using 28 workers of 28 for evacuation
[10.191s][info][gc,phases ] GC(36) Pre Evacuate Collection Set: 0.0ms
[10.191s][info][gc,phases ] GC(36) Evacuate Collection Set: 6.9ms
[10.191s][info][gc,phases ] GC(36) Post Evacuate Collection Set: 5.9ms
[10.191s][info][gc,phases ] GC(36) Other: 0.2ms
[10.191s][info][gc,heap ] GC(36) Eden regions: 286->0(276)
[10.191s][info][gc,heap ] GC(36) Survivor regions: 15->26(38)
[10.191s][info][gc,heap ] GC(36) Old regions: 88->88
[10.191s][info][gc,heap ] GC(36) Humongous regions: 3->1
[10.191s][info][gc,metaspace ] GC(36) Metaspace: 8152K->8152K(1056768K)
[10.191s][info][gc ] GC(36) Pause Young (G1 Evacuation Pause) 391M->114M(508M) 13.075ms
[10.191s][info][gc,cpu ] GC(36) User=0.20s Sys=0.00s Real=0.01s
4、影响垃圾收集性能的因素
影响垃圾收集性能的两个最重要的因素是总可用内存和年轻代在堆中比例。
4.1、总堆
影响垃圾收集性能的最重要因素是总可用内存。由于垃圾收集发生在各代被填满时,因此吞吐量和可用内存量成反比。
注:一下冠以堆增大和缩小、堆布局、默认值等相关讨论,都是基于串行收集器。
-
堆选项影响各代大小
在初始化虚拟机时,将保留堆的整个空间。 可以使用-Xmx
选项指定保留空间的大小。 如果-Xms
参数的值小于-Xmx
参数的值,则不会将所有保留的空间立即提交给虚拟机。 未提交的空间在图中标记为“虚拟”。 堆的不同部分,即老年代和年轻代,可以根据需要增长到虚拟空间的极限。
–XX:NewRatio
代表年轻代与老年代的比例。
Figure 4-1 堆选项
-
堆大小的默认选项
默认情况下,虚拟机会在每次垃圾收集中增大或缩小堆,将活动对象和可用空间的比例保持在特定范围内。
-XX:MinHeapFreeRatio=
<minimum> 和-XX:MaxHeapFreeRatio=
<maximum>将此目标设置为百分比,并且总大小在 –Xms
<min>
和 –Xmx
<max>
之间。
Table 4-1 64位Solaris操作系统的默认选项
Option | Default Value |
---|---|
-XX:MinHeapFreeRatio | 40 |
-XX:MaxHeapFreeRatio | 70 |
-Xms | 6656 KB |
-Xmx | calculated |
默认的最大堆大小是由JVM计算的值。 Java SE中用于并行收集器和服务器JVM的计算现在用于所有垃圾收集器。部分结果是64位平台的最大堆大小的上限。客户端JVM有类似的计算,使得最大堆大小小于服务器JVM。
以下是有关服务器应用程序的堆大小的一般准则:
-
除非遇到延迟问题,否则请尽量为虚拟机提供尽可能多的内存。默认大小通常太小。
-
将
-Xms
和-Xmx
设置为相同的值,可通过从虚拟机中删除最重要的大小决策来提高可预测性。但是,如果做出糟糕的选择,则虚拟机无法进行补偿。 -
通常,在增加处理器数量时增加内存,因为分配可以并行。
- 通过最小化堆大小来减少动态占用空间
如果需要最小化应用程序的动态内存占用(执行期间消耗的最大RAM),则可以通过最小化Java堆大小来实现此目的。 Java SE Embedded应用程序可能需要这样做。
减小 -XX:MaxHeapFreeRatio
和 -XX:MinHeapFreeRatio
参数值。减小-XX:MaxHeapFreeRatio
to 到 10% 和-XX:MinHeapFreeRatio
可以成功地减小堆大小而不会降低太多性能。但是,根据不同应用,结果可能会有很大差异。尝试使用这些参数的不同值,直到它们尽可能低,但仍保持可接受的性能。
此外,指定 -XX:-ShrinkHeapInSteps
它会立即将Java堆减少到目标大小(由 -XX:MaxHeapFreeRatio
指定)。使用此设置可能会降低性能。默认情况下,Java运行时会逐渐将Java堆减少到目标大小,此过程需要多个垃圾收集周期。
4.2、年轻代
在总可用内存之后,影响垃圾收集性能的第二个最有影响的因素是年轻代在堆中比例。
年轻代越大,发生的young gc就越少。但是,对于有限的堆大小,较大的年轻代意味着较小的老年代,这将增加major gc的频率。最佳选择取决于应用程序分配的对象的生命周期分布。
-
年轻代大小选项
默认情况下,年轻代大小由 -XX:NewRatio
控制。
-XX:NewRatio=3
代表年轻代和老年代比例为1:3,即eden+survivor*2占总堆的1/4。
-XX:NewSize
和-XX:MaxNewSize
设置年轻代的大小的最小值和最大值。
-
Survivor 空间大小
-XX:SurvivorRatio
调整survivor空间比例。
-XX:SurvivorRatio=6
将eden和一个survivor空间之间的比率设置为1:6。即,每个survivor空间将是eden大小的六分之一,是年轻代的八分之一(不是七分之一,因为有两个survivor空间)。
如果survivor空间太小,那么复制集合会直接溢出到老年代。如果survivor空间太大,那么它们就没用了。在每次垃圾收集时,虚拟机会选择一个阈值编号,该阈值编号是对象在旧版之前可以复制的次数。选择此阈值是为了使幸存者保持半满。您可以使用日志配置-Xlog:gc,age可用于显示此阈值和新一代对象的年龄。它对于观察应用程序的生命周期分布也很有用。
Table 4-2 Survivor Space Sizing的默认选项值
Option | Server JVM Default Value |
---|---|
| 2 |
| 1310 MB |
| not limited |
| 8 |
年轻代的最大大小是总堆的最大大小和 -XX:NewRatio
参数的值计算的。-XX:MaxNewSize
在未指定时为“not limited”。
以下是服务器应用程序的一般准则:
-
首先确定可以为虚拟机提供的最大堆大小。 然后,根据年轻代尺寸绘制性能指标,以找到最佳设置。
-
请注意,最大堆大小应始终小于计算机上安装的内存量,以避免过多的页面错误和颠簸。
-
-
如果总堆大小是固定的,那么增加年轻代的大小需要减少老年代大小。 保持老年代足够大以容纳应用程序在任何给定时间使用的所有实时数据,加上一些松弛空间(10到20%或更多)。
-
遵循先前对老年代的约束
-
给年轻代足够的内存
-
随着处理器数量的增加,增加年轻代的大小,因为分配可以并行化。
-
5、收集器
5.1、串行收集器
串行收集器使用单个线程来执行所有垃圾收集操作,这使得它相对有效,线程之间没有通信开销。
最适合单处理器积极,因为它无法利用多处理器硬件,尽管它对于具有小数据集(最大约100M)的应用程序的多处理器非常有用。默认情况下,在某些硬件和操作系统配置上选择串行收集器,使用选项 -XX:+UseSerialGC
。
5.2、并行收集器
并行收集器也称为吞吐量收集器,它是类似于串行收集器的分代收集器。串行和并行收集器之间的主要区别在于并行收集器具有多个线程,用于加速垃圾收集。
并行收集器适用于在多处理器或多线程硬件上运行的中型到大型数据集的应用程序,使用选项 -XX:+UseParallelGC
。
并行压缩是一种使并行收集器能够并行执行主要集合的功能。如果没有并行压缩,主要集合将使用单个线程执行,这可能会显著限制可伸缩性。-XX:+UseParallelGC
会默认开启并行压缩。 -XX:-UseParallelOldGC
选项不开启并行压缩。
5.3、最常用的收集器
并发标记清除(CMS)收集器和Garbage-First(G1)垃圾收集器是两个主要并发的收集器。大多数并发收集器用于执行应用程序中一些昂贵代价的工作。
-
G1垃圾收集器:此服务器式收集器适用于具有大量内存的多处理器计算机。它以高概率满足垃圾收集暂停时间目标,同时实现高吞吐量。
默认情况下,在某些硬件和操作系统配置上选择G1,或者可以使用选项
-XX:+UseG1GC
。 -
CMS收集器:此收集器适用于较短垃圾收集暂停且可以与垃圾收集共享处理器资源的应用程序。
使用选项
-XX:+UseConcMarkSweepGC
从JDK 9开始,不推荐使用CMS收集器。
5.4、选择收集器
除非应用程序具有相当严格的暂停时间要求,否则首先运行应用程序并允许VM选择收集器。
如有必要,调整堆大小以提高性能。如果性能仍不符合目标,使用以下指南:
-
如果应用程序具有较小的数据集(最大约100 MB),则选择串行收集器,使用选项
-XX:+UseSerialGC
. -
如果应用程序将在单个处理器上运行且没有暂停时间要求,则选择串行收集器,使用选项
-XX:+UseSerialGC
. -
如果(a)峰值为应用程序性能的第一优先级并且(b)没有暂停时间要求或暂停一秒或更长时间是可接受的,那么让VM选择收集器或选择
-XX:+UseParallelGC
. -
如果响应时间比总吞吐量更重要,并且垃圾收集暂停必须保持短于大约一秒,选择并发收集器,使用选项
-XX:+UseG1GC
或-XX:+UseConcMarkSweepGC
.
这些指南仅提供了选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。 暂停时间对这些因素特别敏感,因此前面提到的一秒的阈值仅是近似的。 并行收集器在许多堆大小和硬件组合上将经历超过一秒的暂停时间。 相反,在某些情况下,并发收集器可能无法使暂停时间短于一秒。
如果推荐的收集器未达到所需性能,则首先尝试调整堆和各代大小以满足所需目标。 如果性能仍然不足,那么尝试使用不同的收集器:使用并发收集器来减少暂停时间,并使用并行收集器来提高多处理器硬件的总吞吐量。
6、并行收集器
并行收集器(也称为吞吐量收集器)是类似于串行收集器的分代收集器。 串行和并行收集器之间的主要区别在于并行收集器具有多个线程,用于加速垃圾收集。
使用命令行选项 -XX:+UseParallelGC
。默认情况下, minor gc 和 major gc都会并行执行,以进一步减少垃圾收集开销。
6.1、并行收集器垃圾收集器线程数
在具有<N>硬件线程且<N>大于8的机器上,并行收集器使用固定比例<N>作为垃圾收集器线程的数量。
对于<N>的大值,该比例约为5/8。在<N>低于8的值时,使用的数字是<N>。在特定的平台上,比例降至5/16。可以使用命令行选项调整垃圾收集器线程的特定数量。在具有一个处理器的主机上,由于并行执行所需的开销(同步),并行收集器的性能可能不如串行收集器。但是,在运行具有中型到大型堆空间的应用程序时,当有两个以上的处理器可用时,通常比串行收集器的性能要好得多。
使用命令行选项 -XX:ParallelGCThreads=
<N>控制垃圾收集器线程数。如果使用命令行选项调整堆,则使用并行收集器获得良好性能所需的堆大小与串行收集器所需的大小相同。但是,启用并行收集器使收集暂停时间更短。因为多个垃圾收集器线程共同参与minor gc,由于在收集期间从年轻代到老年代的晋升,可能会产生一些碎片。minor gc中涉及的每个垃圾收集线程都会保留老年代的一部分空间用于晋升,并且将可用空间划分为“晋升缓冲区”,从而导致碎片效应。减少垃圾收集器线程的数量并增加老年代大小将减少这种碎片效应。
6.2、并行收集器中分代机制
6.3、并行收集器Ergonomics
-XX:+UseParallelGC
选项选择并行收集器,启用自动调整方法,允许指定行为,而不是分代大小及其他低级调整细节。
-
指定并行收集器行为的选项
可以指定最大垃圾收集暂停时间,吞吐量和占用空间(堆大小)。
-
最大垃圾收集暂停时间:使用命令行选项
-XX:MaxGCPauseMillis=
<N>指定最大暂停时间目标。需要<N>毫秒或更短的暂停时间。默认情况下,没有最大暂停时间目标。如果指定了暂停时间目标,则调整堆大小和与垃圾收集相关的其他参数,以试图使垃圾收集暂停时间短于指定值,但是,并不总是满足所需的暂停时间目标。这些调整可能导致垃圾收集器降低应用程序的总吞吐量。 -
吞吐量:吞吐量目标是根据垃圾收集所花费的时间与垃圾收集之外所花费的时间(称为应用程序时间)来衡量的。目标由命令行选项
-XX:GCTimeRatio=
<N>指定,将垃圾收集时间与应用程序时间的比率设置为1 /(1 + <N>)。-XX:GCTimeRatio=19
设置目标为垃圾收集占总时间的1/20或5%。默认值为99,垃圾收集占总时间的1%。 -
占用空间:使用选项
-Xmx
<N>指定最大堆占用空间。此外,只要满足其他目标,收集器就有一个隐含的目标,即最小化堆的大小。
-
并行收集器目标的优先级
目标是最大暂停时间目标,吞吐量目标和最小占用空间目标,并按以下顺序处理目标:
首先满足最大暂停时间目标。 只有在满足后才能解决吞吐量目标。 同样,只有在满足前两个目标之后才考虑最小占用空间目标。
-
并行收集器分代大小调整
收集器保存的平均暂停时间等统计信息在每次收集结束时更新。
然后测试是否满足目标,并对分代大小进行调整。 显示调用垃圾收集除外, System.gc()
的调用在保持统计数据和调整分代大小时被忽略。
增大和缩小分代尺寸是通过增量来完成的,增量和分代大小保持固定百分比,便于分代向期望大小变换。增大和缩小以不同速率完成。默认情况下,增大速率为20%,缩小速率为5%。年轻代增长百分比由 -XX:YoungGenerationSizeIncrement=
<Y> 控制,老年代增长百分比由 -XX:TenuredGenerationSizeIncrement=
<T> 控制。缩小百分比由 -XX:AdaptiveSizeDecrementScaleFactor=
<D>控制,如果增长百分比为 X%,那么缩小百分比为X/D%.
如果收集器决定在启动时增长分代,则会在增量中添加补充百分比。这种补充随着收集的数量而衰减,并且没有长期影响。 补充的目的是提高启动性能。 缩小的百分比没有补充。
如果未满足最大暂停时间目标,则一次只缩小一代的大小。 如果两代的暂停时间都高于目标,那么具有较大暂停时间的代的大小首先缩小。
如果未满足吞吐量目标,则增加两代的大小。 每个都与其对垃圾收集总时间的贡献成比例地增加。 例如,如果年轻代的垃圾收集时间是总收集时间的25%,并且如果年轻代的完全增量将是20%,那么年轻代将增加5%。
-
并行收集器默认堆大小
除非在命令行中指定了初始和最大堆大小,否则它们将根据计算机上的内存量进行计算。默认的最大堆大小是物理内存的四分之一,而初始堆大小是物理内存的1/64。分配给年轻代的最大空间量是总堆大小的三分之一。
并行收集器初始和最大堆大小的规范:
-Xms
初始堆大小 -Xmx
最大堆大小
-XX:+PrintFlagsFinal
选项中结果查看 -XX:MaxHeapSize
java -XX:+PrintFlagsFinal <GC options> -version | grep MaxHeapSize
6.4、并行收集器时间过长、OutOfMemoryError
如果在垃圾收集(GC)中花费了太多时间,则并行收集器会抛出OutOfMemoryError。
如果超过98%的总时间花在垃圾收集上并且不到2%的堆被恢复,则抛出OutOfMemoryError。 此功能旨在防止应用程序长时间运行,同时由于堆太小而很少或没有进度。 如有必要,可以通过在命令行中添加选项 -XX:-UseGCOverheadLimit
来禁用此功能。
6.5、并行收集器测量
并行收集器的详细垃圾收集器输出与串行收集器的输出基本相同。
7、并发收集器
大多数并发收集器与应用程序同时执行部分工作,Java HotSpot VM包括两个主要并发的收集器:
-
Concurrent Mark Sweep (CMS) collector:此收集器适用于较短垃圾收集暂停目标且可以与垃圾收集共享处理器资源的应用程序
-
Garbage-First (G1) garbage collector:此服务器式收集器适用于具有大量内存的多处理器计算机。它以高概率满足垃圾收集暂停时间目标,同时实现高吞吐量。
7.1、并发收集器的开销
大多数并发收集器交换处理器资源(可供应用程序使用)以缩短主要收集暂停时间。
最明显的开销是在集合的并发部分期间使用一个或多个处理器。在N处理器系统上,集合的并发部分使用可用处理器的K / N,其中1 <= K <= ceiling {N / 4}。除了在并发阶段使用处理器之外,还会产生额外的开销以实现并发。因此,并发收集器的垃圾收集暂停通常要短得多,应用程序吞吐量也往往略低于其他收集器。
在具有多个处理器的计算机上,处理器在收集的并发部分期间可用于应用程序线程,因此并发垃圾收集器线程不会暂停应用程序。通常会缩短暂停时间,但应用程序可用的处理器资源也会减少,应该会有一些减速,特别是在应用程序最大限度地使用所有处理内核的情况下。随着N的增加,由于并发垃圾收集使得的处理器资源的减少更少,并发收集的益处增加。
因为在并发阶段期间至少有一个处理器用于垃圾收集,所以并发收集器通常不适用于单处理器(单核)机器。
8、并发标记清除(CMS)收集器
并发标记清除(CMS)收集器专为需要较短垃圾收集暂停且能够在应用程序运行时与垃圾收集器共享处理器资源的应用程序而设计。
通常,具有相对大的长寿命数据集(大型老年代代)并且在具有两个或更多处理器的机器上运行的应用程序倾向于该收集器的使用。 使用命令行选项-XX:+ UseConcMarkSweepGC
启用CMS收集器。
不推荐使用CMS收集器。 强烈考虑使用Garbage-First收集器。
8.1、CMS收集器性能和结构
类似其他收集器,CMS收集器是分代的,minor gc 和 major gc。CMS收集器通过使用独立的垃圾收集线程在执行应用程序线程的同时追踪可达对象来减少暂停时间。
在每个major gc周期中,CMS收集器会在收集开始时暂停所有应用程序线程,在收集的中间位置会再次暂停。第二次暂停时间较长。多个线程在两个暂停期间执行收集工作。一个或多个垃圾收集器线程执行收集的其余部分(包括大多数活动对象的追踪和扫描不可到达的对象)。minor gc可以与正在进行的major gc交替进行,并以类似于并行收集器的方式完成(minor gc是应用程序线程被停止)。
8.2、并发模式失败
CMS收集器使用一个或多个应用程序线程同时运行垃圾收集器线程,目标在于老年代变满之前完成收集。
在正常情况下,CMS收集器在应用程序线程运行时执行大部分跟踪和扫描工作,因此应用程序线程只看到短暂的暂停。但是,如果CMS收集器无法在老年代填满之前完成回收不可到达的对象,或者如果无法使用老年代中的可用空闲块满足分配,则应用程序将暂停收集完成所有收集工作)。应用程序线程都停止了无法同时完成收集称为并发模式失败,表示需要调整CMS收集器参数。如果并发收集被显式垃圾收集( System.gc()
)中断或者为诊断工具提供信息所需的垃圾收集,则提示并发模式中断。
8.3、GC时间过长、OutOfMemoryError
如果在垃圾收集中花费了太多时间,CMS收集器将抛出OutOfMemoryError
,如果在垃圾收集中花费了超过98%的总时间并且恢复少于2%的堆,则抛出 OutOfMemoryError
。
T此功能旨在防止应用程序长时间运行,同时由于堆太小而很少或没有进度。如有必要,可以通过在命令行中添加选项-XX:-UseGCOverheadLimit
来禁用此功能。
该策略与并行收集器中的策略相同,只是执行并发收集所花费的时间不计入98%的时间限制。即,只有在应用程序停止时执行的收集才会计入过多的GC时间。此类收集通常是由于并发模式失败或显式收集请求(例如,对 System.gc()
的调用)。
8.4、CMS收集器和浮动垃圾
与Java HotSpot VM中的所有其他收集器一样,CMS收集器是一个跟踪收集器,至少可以识别堆中的所有可访问对象。
由于应用程序线程和垃圾收集器线程在major gc期间并发运行,因此垃圾收集器线程跟踪的对象随后可能在时间收集过程结束时变得不可访问。这些尚未回收的无法到达的对象被称为浮动垃圾。浮动垃圾的数量取决于并发收集周期的持续时间以及应用程序的参考更新频率(也称为突变)。此外,由于年轻代和老年代是独立收集的,每一代都是另一方的根源。作为一个粗略的指导方针,尝试将老年代的规模增加20%来存放浮动垃圾。一个并发收集周期结束时堆中的浮动垃圾在下一个收集周期内收集。
8.5、CMS收集器暂停
CMS收集器在并发收集周期中暂停两次应用程序。第一次暂停是标记根目录中可直接访问的对象(例如,来自应用程序线程堆栈和寄存器的对象引用,静态对象等)以及堆中的其他位置对象(例如,年轻代)。
第一次暂停被称为初始标记暂停。第二次暂停在并发跟踪阶段结束时,重新标记由于对象中引用的应用程序线程的更新而发现并发跟踪遗漏的对象。第二次暂停被称为重新标记暂停。
8.6、CMS收集器并发阶段
可达对象图的并发追踪发生在初始标记暂停和重新标记暂停之间。
在此并发跟踪阶段期间,一个或多个并发垃圾收集器线程可能正在使用可供应用程序使用的处理器资源。因此,即使应用程序线程未暂停,计算绑定应用程序也可能在此和其他并发阶段期间看到应用程序吞吐量的相应减少。重新标记暂停后,并发清除阶段会收集标识为无法访问的对象。收集周期完成后,CMS收集器会等待,几乎不消耗任何计算资源,直到下一个major gc周期开始。
8.7、启动并发收集周期
使用串行收集器时,只要老年代变满,就会发生major gc,在收集完成时停止所有应用程序线程。CMS收集器在并发收集开始前必须限定时间,以便收集可以在老年代变满前完成。否则,由于并发模式失败,应用程序暂停更长时间。
根据最近的历史记录,CMS收集器估计老年代用尽之前剩余时间以及并发收集周期所需的时间。使用这些动态估计,开始并发收集周期,目的是在老年代用尽之前完成收集周期。这些估计值是为了安全而填充的,因为并发模式失败代价非常大。
如果老年代的占用率超过初始占用率(老年代的百分比),则开始并发收集。此初始占用阈值的默认值约为92%,但该值可能会随发行版的不同而有所变化。可以使用命令行选项-XX:CMSInitiatingOccupancyFraction=
<N>
手动调整此值,其中<N>是老年代大小的整数百分比(0到100)。
8.8、暂停周期
年轻代和老年代收集的停顿独立发生。
它们不重叠,但可以快速连续发生,使得来自一个收集的暂停,紧接着来自另一个收集的暂停,看起来是单个较长的暂停。 为避免这种情况,CMS收集器会在上一次和下一次年轻代暂停之间重新标记暂停。 目前没有对初始标记暂停进行此调度,通常比重新标记暂停短得多。
8.9、CMS收集器测量
-Xlog:gc
输出:
[121,834s][info][gc] GC(657) Pause Initial Mark 191M->191M(485M) (121,831s, 121,834s) 3,433ms
[121,835s][info][gc] GC(657) Concurrent Mark (121,835s)
[121,889s][info][gc] GC(657) Concurrent Mark (121,835s, 121,889s) 54,330ms
[121,889s][info][gc] GC(657) Concurrent Preclean (121,889s)
[121,892s][info][gc] GC(657) Concurrent Preclean (121,889s, 121,892s) 2,781ms
[121,892s][info][gc] GC(657) Concurrent Abortable Preclean (121,892s)
[121,949s][info][gc] GC(658) Pause Young (Allocation Failure) 324M->199M(485M) (121,929s, 121,949s) 19,705ms
[122,068s][info][gc] GC(659) Pause Young (Allocation Failure) 333M->200M(485M) (122,043s, 122,068s) 24,892ms
[122,075s][info][gc] GC(657) Concurrent Abortable Preclean (121,892s, 122,075s) 182,989ms
[122,087s][info][gc] GC(657) Pause Remark 209M->209M(485M) (122,076s, 122,087s) 11,373ms
[122,087s][info][gc] GC(657) Concurrent Sweep (122,087s)
[122,193s][info][gc] GC(660) Pause Young (Allocation Failure) 301M->165M(485M) (122,181s, 122,193s) 12,151ms
[122,254s][info][gc] GC(657) Concurrent Sweep (122,087s, 122,254s) 166,758ms
[122,254s][info][gc] GC(657) Concurrent Reset (122,254s)
[122,255s][info][gc] GC(657) Concurrent Reset (122,254s, 122,255s) 0,952ms
[122,297s][info][gc] GC(661) Pause Young (Allocation Failure) 259M->128M(485M) (122,291s, 122,297s) 5,797ms
CMS 收集(GC ID 657) minor gc (GC IDs 658, 659 and 660),多个minor gc 在并发收集周期中发生。 初始标记暂停表示并发收集周期开始 -> 并发标记 以“Concurrent”开头的行表示并发阶段的开始和结束-> 预清理(并发)为重新标记做准备 -> 重新标记暂停是最后的暂停 -> 并发清除 -> 并发重置,为下一个并发收集做准备。
初始标记暂停通常相对于minor gc 暂停时间较短。 并发阶段(并发标记,并发预清除和并发清除)通常持续时间明显长于minor gc暂停,如CMS收集器输出示例中所示。 但请注意,在这些并发阶段期间不会暂停应用程序。 重新标记暂停的长度通常与minor gc相当。 重新标记暂停受某些应用程序特征的影响(例如,高速率的对象修改会增加此暂停)以及自上次minor gc以来的时间(例如,年轻代中的更多对象可能会增加此暂停)。
9、G1垃圾收集器
9.1、启用G1
Garbage-First垃圾收集器是默认收集器,因此通常不必执行任何其他操作。命令行选项 -XX:+UseG1GC
9.2、基本概念
G1是分代的,增量的,并行的,大部分并发,stop-the-world的垃圾收集器。与其他收集器类似,G1将堆分成年轻代和老年代。空间回收工作集中在年轻代,这是最有效的,老年代偶尔进行空间回收
有些操作是在stop-the-world时执行以提高吞吐量。其他需要花费更多时间停止应用程序的操作,例如全局标记等整堆操作,是与应用程序并行执行的。为了使空间回收缩短 stop-the-world 停顿时间,G1逐步并行地进行空间回收。 G1通过跟踪先前应用程序行为和垃圾收集暂停的信息来建立相关成本的模型,从而实现可预测性。它使用此信息来确定暂停中完成的工作的大小。例如,G1首先在最有效的区域中回收空间(几乎充满垃圾的区域)。
G1主要通过疏散回收空间:在选定的存储区域内找到的活动对象被复制到新的存储区域,在此过程中压缩它们。疏散完成后,活动对象先前占用的空间将被重用,以供应用程序分配。
Garbage-First收集器不是实时收集器。它试图在较长时间内以高概率满足设定的暂停时间目标,但对于给定的暂停并不总是绝对确定。.
-
堆布局
G1将堆分区为一组大小相等的堆区域,每个区域都是一个连续的虚拟内存区域,如图9-1所示。 区域是内存分配和内存回收的单位。 在任何给定时间,这些区域中的每一个都可以是空的(浅灰色),或者分配给特定的一代(年轻代或老年代)。 随着内存请求的进入,内存管理器会分发空区域。 内存管理器将它们分配给一代,然后将它们作为可以自行分配的可用空间返回给应用程序。
Figure 9-1 G1 垃圾回收器堆布局
年轻代包含eden区域(红色)和survivor区域(红色“S”)。这些区域提供与其他收集器中的相应连续空间相同的功能,不同之处在于G1中这些区域通常以不连续的模式布置在存储器中。旧区域(淡蓝色)构成了老年代。对于跨越多个区域的对象,老年代区域可能是巨大的(淡蓝色“H“)。
应用程序总是分配给年轻代,即eden区域,除了大对象直接分配给老年代。
G1垃圾收集暂停可以回收整个年轻代的空间,及部分老年代区域。在暂停期间,G1将此集合中的对象复制到堆中的一个或多个不同区域。对象的目标区域取决于该对象的源区域:整个年轻代被复制到survivor或老年代区域,使用老化将老年代区域中的对象复制到老年代其他不同的区域。
-
垃圾收集周期
总的来说,G1收集器在两个阶段之间交替。 young-only 阶段包含垃圾收集,逐渐填充当前可用内存和老年代的对象。 空间回收(space-reclamation)阶段是除了处理年轻代之外,G1逐步回收老年代的空间。 然后循环重新开始,回到young-only 阶段。
Figure 9-2 垃圾收集周期概述
以下列表描述了G1垃圾回收周期的阶段,暂停和阶段之间的转换:
-
Young-only 阶段:这个阶段从一些young-only收集开始,将对象提升到老年代。当老年代占用率达到某个阈值,即启动堆占用阈值,young-only阶段和space-reclamation阶段之间开始过渡。此时,G1开始初始标记young-only收集,不再是常规的young-only收集。
-
初始标记 : 除了执行常规的young-only收集之外,这类收集还会启用标记过程。并发标记确定老年代区域中所有当前可达的对象,以便在下一个space-reclamation阶段保留。 标记尚未完成时,young gc 仍会出现。标记在两个stop-the-world停顿(重新标记、清理)后完成。
-
重新标记:此暂停最终确定标记本身,并执行全局应用处理和类卸载。在重新标记和清理之间,G1并发计算活跃信息,用于清除暂停,并更新内部数据结构。
-
清除:此暂停会完全回收空的区域,并确定是否开始space-reclamation阶段。如果开始space-reclamation,young-only阶段在一个young-only收集后结束。
-
-
Space-reclamation 阶段:该阶段由多个混合收集组成,除了年轻代区域之外,还可以清除老年代区域的对象。当G1发现清除老年代区域不会产生足够的自由空间时,space-reclamation阶段停止。
在space-reclamation之后,收集周期将以另一个young-only阶段重新开始。 如果应用程序在收集活动信息时内存不足,则G1会像其他收集器一样执行就地stop-the-world全堆压缩(Full GC)。
9.3、内部实现
-
确定启动堆占用率
启动堆占用百分比 (IHOP)是出发初始标记收集的阈值,被定义为老年代大小的百分比。
G1默认情况下通过标记所花费的时间以及在标记周期内老年代中分配多少内存来自动确定最佳IHOP。 此功能称为自适应IHOP。 如果此功能处于活动状态,则选项 -XX:InitiatingHeapOccupancyPercent
将初始值确定为当前老年代的大小的百分比,如果没有足够的观察值来对启动堆占用阈值进行良好预测,使用选项-XX:-G1UseAdaptiveIHOP
关闭G1的此行为。
在这种情况下, -XX:InitiatingHeapOccupancyPercent
的值始终确定此阈值。
自适应IHOP尝试设置初始堆占用百分比,当老年代占用率为当前最大值生减去-XX:G1HeapReservePercent
(额外缓冲区)的值时,space-reclamation阶段的第一个混合垃圾收集开始。
-
标记
G1标记使用称为初始快照(SATB)的算法。 它在初始标记暂停时获取堆的虚拟快照,在标记开始时存活的所有对象被认为是在剩余标记中存活的对象。 这意味着在标记期间变为死亡(无法访问)的对象在space-reclamation时仍被认为是存活的(有一些例外)。 与其他收集器相比,这可能会导致错误地保留一些额外的内存。 但是,SATB会在重新标记暂停期间减少延迟。 在该标记期间过于保守地考虑的活动对象将在下一个标记期间被回收。
-
堆空间非常小的情况
当应用程序保持如此大的内存以便疏散无法找到足够的空间来复制时,就会发生清除失败。清除失败时G1通过保留已经移动新位置的对象,不复制任何尚未移动的对象,只调整对象之间的引用来完成收集。清除失败会产生一些额外的开销,但通常与其他young gc一样快。在清除失败的垃圾收集之后,G1将正常恢复应用程序而不采取任何其他措施。 G1假定清除失败发生在垃圾收集结束时,即,大多数对象已经被移动,并且剩下足够的空间来继续运行应用程序,直到标记完成并开始空间回收。如果这个假设不成立,那么G1将安排一个完整的GC,执行整个堆的就地压缩,这可能会很慢。
- 大(Humongous)对象
大对象是大于或等于半个区域大小的对象。除非使用 -XX:G1HeapRegionSize
选项进行设置,否则当前区域大小按照符合人体工程学的方式确定。
大对象有时会以特殊方式处理:
- 每个大对象都被分配为老年代的一系列连续区域。对象本身的起始始终位于该序列中第一个区域的开头。序列的最后一个区域中的任何剩余空间都将丢失,直到整个对象被回收为止。
- 通常,只有在清理暂停期间标记结束时才会回收大对象,如果它们无法访问则可以full gc期间回收。但是,对于基本类型的数组,例如bool,各种整数和浮点值,如果在任何类型的垃圾收集暂停时没有被许多对象引用,G1会尝试回收大量对象。默认情况下启用此行为,但可以使用选项
-XX:G1EagerReclaimHumongousObjects
禁用它。 - 大对象的分配可能导致垃圾收集暂停过早发生。 G1在每个大对象分配时检查初始堆占用阈值,并且如果当前占用率超过该阈值,则立即强制初始标记年轻收集。
- 即使在full gc期间,大对象也不会移动。这可能导致过早、缓慢的full gc或意外的内存不足情况(由于区域空间的碎片而丢失大量可用空间)。
-
young-only阶段分代大小
在young-only的阶段,收集区域的集合(收集集)仅由年轻代区域组成。 G1在young-only收集结束后调整年轻代大小。 这样,G1可以满足使用-XX:MaxGCPauseTimeMillis
设置的暂停时间目标和 -XX:PauseTimeIntervalMillis
基于实际暂停时间的长期观察。 考虑相似规模的年轻代清除需要多长时间。 包括在收集期间必须复制多少对象以及这些对象之间的互连方式等信息。
如果没有另外约束,则G1自适应地调整 -XX:G1NewSizePercent
和 -XX:G1MaxNewSizePercent
年轻代大小之间以满足暂停时间的目标。
-
Space-Reclamation阶段分代大小
在Space-Reclamation阶段,G1尝试在单个垃圾收集暂停中最大化在老年代中回收的空间量。年轻代的大小设置为允许的最小值,通常由 -XX:G1NewSizePercent
确定,并且添加任何用于回收空间的老年代区域,直到G1确定添加更多区域将超过暂停时间目标。在特定的垃圾收集暂停中,G1按照回收效率,获得最终收集集的剩余可用时间的顺序增加老年代区域。
每个垃圾收集所需的老年代区域的数量最小值受要收集的潜在候选老年代区域(收集集合候选区域)的数量限制,除以由 -XX:G1MixedGCCountTarget
确定的空间回收阶段的长度。集合集候选区域都是老年代区域,其占用率低于Space-Reclamation阶段开始时的 -XX:G1MixedGCLiveThresholdPercent
。
当收集集候选区域中可回收的剩余空间量小于 -XX:G1HeapWastePercent
设置的百分比时,该阶段结束。
9.4、默认参数
Option and Default Value | Description |
---|---|
| 最大暂停时间目标 |
| 最大暂停时间间隔目标。 默认情况下,G1不设置任何目标,允许G1在极端情况下连续执行垃圾收集。 |
| 垃圾收集暂停期间用于并行工作的最大线程数。 如果进程可用的CPU线程数小于或等于8,则使用该线程数。 否则添加大于最终线程数五分之八的线程数。 |
| 用于并发工作的最大线程数。 默认情况下,此值为 |
| 用于控制启动堆占用的默认值。打开自适应调整,对于前几个收集周期G1的默认值将使用老年代45%的占用率作为标记开始阈值。 |
| 堆区域大小基于初始和最大堆大小。 这个堆包含大约2048个堆区域。 堆区域的大小可以在1到32 MB之间变化,并且必须是2的幂。 |
| 年轻代的总大小,在这两个值之间变化,作为当前使用的Java堆的百分比。 |
| 集合中允许的未回收空间将候选项设置为百分比。 如果集合集合候选中的可用空间低于该空间,则G1将停止空间回收阶段。 |
| space-reclamation阶段的预期长度。 |
| 在space-reclamation阶段,活动对象占老年代区域的百分比超过此值将不会进行回收。 |
Note:
<ergo>
表示根据环境确定符实际值。
-
与其他收集器比较
- 并行GC只能作为一个整体来压缩和回收老年代的空间。 G1逐步将这项工作分配到多个更短的收集中。这大大缩短了暂停时间,可能会降低吞吐量。
- 与CMS类似,G1并发执行老年代空间回收的一部分。但是,CMS无法对老年代堆进行碎片整理,最终会遇到长的Full GC。
- G1可能表现出比其他收集器更高的开销,由于其并发性质而影响吞吐量。
由于它的工作原理,G1有一些独特的机制来提高垃圾收集效率:
- G1可以在任何收集过程中回收老年代的一些完全空旷的大面积区域。这可以避免许多其他不必要的垃圾收集,从而不需要太多努力就可以释放大量空间。
- G1可以选择并发在Java堆上删除重复的字符串。
-XX:-G1EagerReclaimHumongousObjects
始终启用从老年代回收空的大对象。
-XX:+G1EnableStringDeduplication
启用字符串重复数据删除(默认情况下禁用)。
10 、G1垃圾收集器调优
10.1、通用建议
一般建议使用G1及其默认设置,最终为其提供不同的暂停时间目标,并根据需要使用-Xmx设置最大Java堆大小。
G1默认值与其他任何收集器的平衡方式不同。 G1在默认配置中的目标既不是最大吞吐量也不是最低延迟,而是在高吞吐量下提供相对较小的暂停。但是,G1的递增回收堆中的空间和暂停时间控制的机制在应用程序线程和空间回收效率中都会产生一些开销。
如果需要高吞吐量,使用 -XX:MaxGCPauseMillis
放宽暂停时间目标或提供更大的堆。如果延迟是主要要求,则修改暂停时间目标。避免使用 -Xmn
,
-XX:NewRatio
等选项将年轻代大小限制为特定值,因为年轻代大小是G1允许其满足暂停时间的主要手段。将年轻代大小设置为确定值会覆盖并实际禁用暂停时间控制
10.2、从其他收集器转换到G1
通常,当从其他收集器(特别是CMS收集器)移动到G1时,首先删除影响垃圾收集的所有选项,并仅使用 -Xmx
和可选的 -Xms
设置暂停时间目标和总体堆大小。
许多选项对其他收集器以某种特定方式响应是有用的,根本没有效果,甚至降低吞吐量和满足暂停时间目标的可能性。如设置年轻代的大小,完全阻止G1调整年轻代的大小以达到暂停时间的目标。
10.3、提升G1性能
G1旨在提供良好的整体性能,而无需指定其他选项。但是,有些情况下,默认启发式或默认配置会提供次优结果。在案例分析的基础上,应用程序级优化可能比调整VM以更好地执行更有效,例如,通过完全避免使用寿命较短的对象来解决某些问题。出于诊断目的,G1提供全面的日志记录。一个好的开始是使用-Xlog:gc*=debug
选项,然后根据需要优化输出。该日志提供有关垃圾收集活动暂停期间及之外的详细概述。包括收集的类型和在暂停的特定阶段花费的时间。
常见性能问题:
-
观察完整的垃圾收集
完整堆垃圾收集(full gc)通常非常耗时。通过在日志中找到“Pause Full (Allocation Failure) ”字样,可以检测到l老年代中堆占用率过高导致的 full gc。full gc 通常在to-space exhausted
指示的清除失败垃圾收集之前。发生full gc的原因是应用程序分配了太多无法快速回收的对象。通常并发标记无法及时完成以开始空间回收阶段。大对象的内存分配可能会加剧进入full gc的可能性。由于这些对象在G1中的分配方式,它们可能会占用比预期更多的内存。为确保并发标记按时完成,可以通过降低老年代的分配率,或给予并发标记更多的时间来完成。G1提供了几种选择更好地处理这种情况:
- 如果Java堆上有大量的大对象,那么
gc+heap=info
日志会显示大区域数量。每次垃圾收集后,最好的选择是减少对象的数量。可以通过使用-XX:G1HeapRegionSize
选项增加区域大小来实现此目的。当前选定的堆区域大小将打印在日志的开头。 - 增加Java堆的大小。这通常会增加标记完成的时间。
-
-XX:ConcGCThreads
增加并发标记线程的数量 - 强制G1开始提前标记。 G1根据先前的应用程序行为自动确定初始堆占用百分比(IHOP)阈值。如果应用程序行为发生更改,则这些预测可能是错误的。有两种选择:通过修
-XX:G1ReservePercent
来增加自适应IHOP计算中使用的缓冲区,从而降低何时开始空间回收的目标占用率,或者,通过使用-XX:-G1UseAdaptiveIHOP
和-XX:InitiatingHeapOccupancyPercent
手动设置IHOP来禁用IHOP的自适应计算。
除分配失败之外,其他原因通常是应用程序或某些外部工具导致堆满了,触发full gc。如果是System.gc()
,无法修改应用程序源,则可以通过使用-XX:+ExplicitGCInvokesConcurrent
来缓解full gc的影响,或者是设置 -XX:+DisableExplicitGC
让VM忽略。外部工具仍会强制使用full gc,只能通过不请求它们来删除。
-
大对象碎片
在所有Java堆内存耗尽之前,会发生full gc,以找到一组连续的区域。 在这种情况下,可能的选项是通过使用选项-XX:G1HeapRegionSize
来增加堆区域大小,以减少大量对象的数量,或增加堆的大小。 在极端情况下,虽然有足够可用内存(不连续),G1没有足够的连续空间来分配对象。 如果full gc无法回收足够的连续空间,则会导致VM退出。 除了上述减少大对象分配或增加堆之外,没有其他选择。
-
延迟优化
对于每个垃圾收集暂停,gc+cpu=info
日志输出一行,其中包含来自操作系统的信息,并详细说明暂停时间的花费时间。如 User=0.19s Sys=0.00s Real=0.01s
。
用户时间是在VM代码中花费的时间,系统时间是在操作系统中花费的时间,实际暂停时间是在暂停期间经过的绝对时间。如果系统时间相对较高,则通常环境因素导致的。
系统时间过长:
- VM从操作系统内存分配或回收内存可能会导致不必要的延迟。通过使用选项
-Xms
和-Xmx
,
将最小和最大堆大小设置为相同值来避免延迟,并使用-XX:+AlwaysPreTouch
预先接触所有内存以将此工作移至VM启动阶段。 - 在Linux中,透明大页面(THP)功能将小页面合并成大页面往往会阻碍随机进程,而不仅仅是在暂停期间。由于VM分配并维护了大量内存,因此VM中长时间停顿的进程的风险高于平常。
- 写入日志输出可能会停止一段时间,因为某些后台任务会间歇性地占用写入日志的硬盘的所有I / O带宽。考虑为日志或其他存储使用单独的磁盘,例如使用内存支持的文件系统。
实际暂停时间比其他的总和大得多,可能VM在可能过载的机器上没有获得足够的CPU时间。
引用对象处理时间过长:
有关处理引用对象所用时间的信息显示在Ref Proc和Ref Enq阶段。在Ref Proc阶段,G1根据其特定类型的要求更新引用对象的指示对象。在Ref Enq中,如果引用对象死亡,将引用对象排入各自的引用队列。如果这些阶段花费的时间太长,使用选项 -XX:+ParallelRefProcEnabled
.来启用这些阶段的并行化。
Young-Only收集时间过长:
young-only,young gc时间同年轻代空间成正比,同需要复制的集合中的活动对象的数量成正比。 如果评估收集集合阶段花费的时间过长,特别是对象拷贝子阶段,则减少-XX:G1NewSizePercent
。 减小了年轻代的最小大小,允许更短的暂停。如果应用性能,特别是在收集中存活的对象的数量突然改变,会触发年轻代大小调整。能会导致垃圾收集暂停时间出现峰值。 使用 -XX:G1MaxNewSizePercent
减少最大年轻代大小可,限制年轻代的最大大小,减少暂停期间需要处理的对象数量。
混合收集时间过长:混合集合用于回收老年代的空间。混合收集集合包含年轻代和老年代的区域。通过启用 gc+ergo+cset=trace
日志输出,获取有关年轻代或老年代区域消除对暂停时间的影响信息。根据预测的年轻代区域时间和预测的老年代区域时间,为了减少老年代区域对暂停时间的影响,G1提供了三个选项:
-XX:G1MixedGCCountTarget
扩展老年代垃圾回收区域。-XX:G1MixedGCLiveThresholdPercent
将大于比例阈值的回收区域放入候选集中,避免收集占用相对较长时间的区域。-XX:G1HeapWastePercent
提前停止老年代空间回收,G1不会收集高度占用区域。
注意,最后两个选项会减少收集集合候选区域的数量,其中包含当前空间回收阶段可以回收的空间。 G1可能无法在老年代中回收足够的空间以进行持续操作,交给后续的空间回收阶段收集。
更新RS(Remember Set)和扫描RS时间过长:
为了使G1能够清除单个老年代区域,G1跟踪跨区域引用的位置,即从一个区域指向另一个区域的引用。指向给定区域的一组跨区域引用称为该区域的记忆集。移动区域内容时,必须更新记忆集。区域记忆集的维护主要是并发的。出于性能目的,当应用程序在两个对象之间设置新的跨区域引用时,G1不会立即更新区域的记忆集。记忆集更新请求通过延迟和批处理提升效率。G1需要完整的记忆集以进行垃圾收集,因此垃圾收集的更新RS阶段处理任何未完成的记忆集更新请求。 扫描RS阶段在记忆集中搜索对象引用,移动区域内容,然后将这些对象引用更新为新位置,这两个阶段可能需要很长时间。
使用选项 -XX:G1HeapRegionSize
调整堆区域的大小会影响跨区域引用的数量以及记忆集的大小。处理区域的记忆集是垃圾收集工作的重要部分,对最大暂停时间有直接影响。较大的区域往往具有较少的跨区域引用,处理它们的相对工作量减少,同时,较大的区域意味着每个区域更多的活动对象,增加了其他阶段的时间。
G1尝试并发处理记忆集更新, -XX:G1RSetUpdatingPauseTimePercent
更新RS阶段允许占用的最大暂停时间的百分比,通过减小该值,G1同时执行更多记忆集更新工作。
分配大对象的应用程序中的虚假更新RS时间过长,可能是由于通过批处理来减少并发记忆集更新工作的优化引起的。如果创建此类批处理的应用程序恰好在垃圾收集之前发生,那么垃圾收集必须在更新RS暂停时间处理所有工作。使用 -XX:-ReduceInitialCardMarks
来禁用此行为。
扫描RS时间取决于G1执行的压缩量,以减少记忆集存储大小。记忆集存储在内存中越紧凑,在垃圾回收期间检索存储值所花费的时间就越多。 G1会自动执行此压缩,称为记忆集粗化,同时根据该区域记忆集的当前大小更新记忆集。在最高压缩级别,检索实际数据可能非常慢。选项-XX:G1SummarizeRSetStatsPeriod
与gc+remset=trace
级别日志记录一起显示是否发生此粗化。如果是这样,那么之前的GC Summary中Did <X> coarsenings
的 X
显示高值。可以显着增加 -XX:G1RSetRegionEntries
选项以减少这些粗化的数量。避免在生产环境中使用此详细的记忆集记录,因为收集此数据会花费大量时间。
-
吞吐量调优
G1的默认策略在吞吐量和延迟之间保持平衡。但是,有些情况下需要更高的吞吐量。除了如前面部分所述减少总暂停时间之外,可以减少暂停的频率。使用-XX:MaxGCPauseMillis
增加最大暂停时间。自适应分代大小调整自动调整年轻代的大小,影响暂停的频率。如果不行,特别是在空间回收阶段,使用 -XX:G1NewSizePercent
增加最小年轻代大小将强制G1执行此操作。 -XX:G1MaxNewSizePercent
(允许的最大年轻代大小)可能会通过限制年轻代的大小来限制吞吐量。通过查看 gc+heap=info
日志的区域摘要输出诊断。在这种情况下,eden区和survivor区的综合百分比接近 -XX:G1MaxNewSizePercent
占地区总数的百分比。考虑增加-XX:G1MaxNewSizePercent
增加吞吐量的另一个选择是减少并发工作量,并发记忆集更新通常需要大量CPU资源。增加-XX:G1RSetUpdatingPauseTimePercent
将工作从并发操作移动到垃圾收集暂停。在最坏的情况下,可以通过设置-XX来禁用并发记忆集更新: -XX:-G1UseAdaptiveConcRefinement
-XX:G1ConcRefinementGreenZone=
2G
-XX:G1ConcRefinementThreads=
0
。禁用此机制,并将所记忆集更新工作移动到下一个垃圾收集暂停中。 -XX:+UseLargePages
启用大页面,提高吞吐量通过禁用来最小化堆大小调整工作,将选项 -Xms
和 -Xmx
设置为相同的值。此外,使用 -XX:+AlwaysPreTouch
将操作系统工作移动到VM启动时间。从而减少暂停时间。
-
堆空间调优
与其他收集器一样,G1的目的是调整堆的大小,以便在垃圾收集中花费的时间低于-XX:GCTimeRatio
选项确定的比率。 调整此选项可使G1满足要求。
-
默认参数
Option and Default Value | Description |
---|---|
| 并发记忆集更新(细化)使用这些选项来控制并发细化线程的工作分布。 G1为这些选项选择符合人体工程学的值,以便在垃圾收集暂停中花费 |
| 将对初始对象分配的并发记忆集更新(细化)工作进行批处理。 |
| java.lang.Ref。*实例的处理是否应该由多个线程并行完成 |
| 在更新RS阶段中更新剩余的记忆集占总垃圾收集时间百分比。 G1使用此设置控制并发记忆集更新的数量。 |
| 生成记忆集汇总报告。 将此值设置为零以禁用。 生成记忆集报告是一项代价高昂的操作,因此只有在必要时才能使用,并且具有相当高的价值。 使用 |
| 应用程序中花费时间与垃圾收集的目标时间比值。 确定在增加堆之前,垃圾收集花费时间占总时间的 |
11、其他因素
11.1、强引用、弱引用、软引用、虚引用
某些应用程序通过使用finalization和weak,soft或phantom引用与垃圾收集进行交互。
这些功能可以在Java编程语言级别实现性能优化。如依靠finalization来关闭文件描述符,使得外部资源(描述符)依赖于垃圾收集的快速性。
11.2、显示垃圾收集
显示调用System.gc()
完成垃圾收集。
强制在可能没有必要时进行major gc(例如,minor gc就足够时),通常应该避免。 -XX:+DisableExplicitGC
禁用,VM忽略 System.gc()
的调用。
显式垃圾收集用途之一是使用远程方法调用(RMI)中分布式垃圾收集(DGC)。使用RMI的应用程序引用其他虚拟机中的对象。在没有调用本地堆的垃圾收集的情况下,无法在这些分布式应用程序中收集垃圾,因此RMI会定期强制执行完整收集。使用选项控制这些收集的频率,如下所示:
java -Dsun.rmi.dgc.client.gcInterval=3600000
-Dsun.rmi.dgc.server.gcInterval=3600000 ...
此示例指定每小时一次显式垃圾回收,而不是默认的每分钟一次。 但是,这可能导致某些对象需要更长的时间才能回收。 如果DGC没有时间上限,设置为与Long.MAX_VALUE
。
11.3、软引用
软引用在服务器虚拟机中比在客户端中保持更长时间。
可以使用命令行选项 -XX:SoftRefLRUPolicyMSPerMB=
<N>
来控制清除率,该选项指定堆中可用空间每个兆字节的软引用保持活动状态(不再试强可达)的毫秒数(ms) 。 默认值为每兆字节1000毫秒,这意味着对于堆中的每兆字节可用空间,软引用将在(收集对象的最后一次强引用之后)存活1秒钟。 这是一个近似数字,因为软件引用仅在垃圾收集期间被清除。
11.4、类元数据
Java类在Java Hotspot VM中具有内部表示,并称为类元数据。
在旧版本的Java Hotspot VM中,类元数据是在永久代中分配的。从JDK 8开始,删除了永久代,并且在本机内存中分配了类元数据。默认情况下,可用于类元数据的本机内存量是无限制的。使用选项 -XX:MaxMetaspaceSize
为用于类元数据的本机内存量设置上限。
Java Hotspot VM显式管理元数据空间。 从操作系统请求空间,然后分成块。 类加载器从其块中为元数据分配空间(块被绑定到特定的类加载器)。 类加载器卸载类时,其块将被回收以供重用或返回到操作系统。 元数据使用由 mmap
分配的空间,而不是 malloc
。
如果启用 -XX:UseCompressedOops
并使用 -XX:UseCompressedClassesPointers
,使得本机内存两个逻辑上不同的区域用于类元数据。
-XX:UseCompressedClassPointers
使用32位偏移量来表示64位进程中的类指针, -XX:UseCompressedOops
用于Java对象引用。
为这些压缩类指针(32位偏移)分配区域。 使用 -XX:CompressedClassSpaceSize
设置区域的大小,默认情况下为1GB。
压缩类指针的空间保留为初始化时由-XX:mmap
分配的空间,并根据需要增大。
-XX:MaxMetaspaceSize
指定压缩类空间和其他类元数据的空间之和。
卸载相应的Java类时,将释放类元数据。垃圾收集卸载Java类,释放类元数据。当为类元数据占用空间达到一定水平(高位线)时,会出发垃圾收集。在垃圾收集之后,可以根据从类元数据中释放的空间量来升高或降低高水位标记。提高高位线,以免过早引起另一次垃圾收集。高位线最初设置为命令行选项 -XX:MetaspaceSize
的值。根据选项-XX:MaxMetaspaceFreeRatio
和 -XX:MinMetaspaceFreeRatio
升高或降低。如果类元数据的可用空间占类元数据总空间的百分比大于 -XX:MaxMetaspaceFreeRatio
,则高位线降低。如果它小于-XX:MinMetaspaceFreeRatio
,那么高位线升高。
-XX:MetaspaceSize
选项指定更高的值,以避免为类元数据引发早期垃圾收集。 为应用程序分配的类元数据量取决于应用程序,不存在选择 -XX:MetaspaceSize
的一般准则。 -XX:MetaspaceSize
的默认大小取决于平台,范围从12 MB到大约20 MB。
堆中用于元数据的空间的信息:
[0,296s][info][gc,heap,exit] Heap
[0,296s][info][gc,heap,exit] garbage-first heap total 514048K, used 0K [0x00000005ca600000, 0x00000005ca8007d8, 0x00000007c0000000)
[0,296s][info][gc,heap,exit] region size 2048K, 1 young (2048K), 0 survivors (0K)
[0,296s][info][gc,heap,exit] Metaspace used 2575K, capacity 4480K, committed 4480K, reserved 1056768K
[0,296s][info][gc,heap,exit] class space used 238K, capacity 384K, committed 384K, reserved 1048576K
在以 Metaspace
,开头的行中, used
值是已使用加载类的空间量。capacity
值是当前分配的块中可用于元数据的空间。 committed
值是可用于块的空间量。reserved
值是元数据保留(但不一定是已提交)的空间量。 以 class space
开头的行包含压缩类指针的元数据的相应值。