logo头像
Snippet 博客主题

GC调优基础之堆大小

本文于307天之前发表,文中内容可能已经过时

调整堆大小

GC调整的第一堂课是调整应用程序堆的大小。关于堆大小的调整还有更高级的话题,不过作为第一步,我们首先讨论如何设置总体堆的大小。

与其他的性能问题一样,选择堆的大小其实是一种平衡。如果分配的堆过于小,程序的大部分时间可能都消耗在GC上,没有足够的时间去运行应用程序的逻辑。但是,简单粗暴地设置一个特别大的堆也不是解决问题的方法。

GC停顿消耗的时间取决于堆的大小,如果增大堆的空间,停顿的持续时间也会变长。这种情况下,停顿的频率会变得更少,但是它们持续的世界会让程序的整体性能变慢。

使用超大堆还有另一个风险。操作系统使用虚拟内存机制管理机器的物理内存。一台机器可能有8G的物理内存,不过操作系统可能让你感觉有更多的可用内存。虚拟内存的数量取决于操作系统的设置,譬如操作系统可能让你感觉到它的内存达到了16G。操作系统名为”交换”(swapping,称之为分页,虽然这两者在技术上存在着差异,但是这些差异,在这里不影响我们的讨论)。你可以载入需要16GG内存的应用程序,操作系统在需要时会将程序运行时不活跃的数据由内存复制到磁盘。再次需这部分内存的内容时,操作系统再将它们由磁盘重新载入到内存(为了腾出空间,通常它会先将另一部分内存的内容复制到磁盘)。

系统中运行着大量不同的应用程序时,这个流程工作得很不顺畅,因为大多数的应用程序不会同时处于活跃状态。但是,对于Java应用,它工作得并不是很好。如果一个Java应用使用了这个系统上大约12G的堆,操作系统可能在RAM上分配8G的堆空间,另外4G的空间存在于磁盘(这个假设对实际情况进行了一些简化,因为应用程序也会使用部分的RAM)。JVM不会了解这些:操作系统完全屏蔽了内存交互的细节。这样,JVM愉快的地填满了分配给它的12G堆空间。但这样就导致了严重的性能问题,因为操作系统需要将相当一部分的数据由磁盘交换到内存(这是一个昂贵操作的开始)。

更糟糕的是,这种原本期望一次性的内存交换操作在Full GC时一定会再次重演,因为JVM必须访问整个堆的内容。如果Full GC时系统发生内存交换,停顿时间会以正常停顿时间数个量级的方式增长。类似的,如果使用Concurrent收集器,后台线程在回收堆时,它的速度也可能会被拖慢,因为需要等待从裁判复制数据到内存,结果导致发生代价昂贵的并发模式失效。

因此,调整堆大小时首要的原则就是永远不要将堆的容量设置得比机器的物理内存还大。另外,如果同一台机器上运行着多个JVM实例,这个原则适用于所有堆的总和。除此之外,你还需要为JVM自身以及机器上其他的应用程序预留一部分的内存空间:通常情况下,对于普通的操作系统,应该预留至少1G的内存空间。

堆的大小由2个参数值控制:分别是初始值(通过-Xms N设置)和最大值(通过 -Xmx N设置)。默认值的调节取决于多个因素,包括操作系统类型、系统内存大小、使用的JVM。其他的命令标志也会对该值造成影响,堆大小的调节是JVm自适应调优的核心。

JVM的目标是一句系统可用的资源情况找到一个”合理的”默认初始化值,当且仅当应用程序需要更多的内存(依据垃圾回收时消耗的时间来决定)时将堆的大小增大到一个合理的最大值。到目前为止,JVM的高级调优标志以及调优细节都没有提及。为了让大家有个感性的认识,我们列出了堆大小的默认最大值和最小值供大家参考,参见如下表。(为了使内存对齐,JVM会对这些值进行圆整操作;所以GC日志中输出的大小可能与表中给出的值并不完全一致)。

操作系统及JVM类型 初始堆大小(Xms) 最大堆大小(Xmx)
Linux/Solaris,32位客户端 16MB 256MB
Linux/Solaris,32位客户端 64MB 取1GB和物理内存大小1/4二者中的最小值
Linux/Solaris,32位客户端 取512MB和物理内存大小1/64二者中的最小值 取32GB和物理内存大小1/4二者中的最小值
MacOS,64位服务器型JVM 64MB(数据有误,应该是256MB) 取1GB和物理内存大小1/4二者中的最小值
Windows,32位客户端JVM 16MB 256MB
Windows,64位服务端JVM 64MB 取1GB和物理内存大小1/4二者中的最小值

如果机器的物理内存少于192MB,最大堆的大小会是物理内存的一半(大约96MB,或更少)。

堆大小具有初始值和最大值的这种设计让JVM能够根据实际的负荷情况更灵活地调整JVM的行为。如果JVM发现使用初始的堆大小,频繁地发生GC,它就会尝试增大堆的空间,直到JVM的GC的频率回归到正常的范围,或直到堆大小增大到它的上限值。

对很多应用来说,这意味着堆的大小不再需要调整了,实际上,你只需要为你选择的GC算法设定性能目标:譬如你能忍受的停顿的持续时间、你期望垃圾回收在整个时间中所占用的百分比等。

如果应用程序在GC时消耗了太长的时间,你很有可能需要使用-Xmx标志增大堆的大小。选择什么样的大小没有一个硬性的或简单的规则。一个经验法则是完成Full GC后,应该释放出70%的空间(30%的空间仍然占用)。

注意,即使你显示地设置了堆的最大容量,还是会发生堆的自动调节:初始时堆以默认的大小开始运行,为了达到根据垃圾收集算法设置的性能目标,JVM会逐步增大堆的大小。将堆的大小设置得比实际需要更大不一定会带来性能损耗:堆并不会无限地增大,JVM会调节堆的大小直到其满足GC的性能目标。

另一方面,如果你确切地了解应用程序需要多大的堆,那么你可以将堆的初始化和最大值设置一致数值(譬如:-Xms1024M -Xmx1024M)。这种设置能稍微提高GC的运行效率,因为它不再需要估算堆是否需要调整大小。

堆内存纠错

在上述表我们知道了不同操作系统、不同JVM、不同物理内存的默认堆大小。但是实际上,Mac OS这个数据有问题的。我的Mac Pro 2013的配置物理内存是16G、64位的JVM。

下面两个命令用于查看当前JRE默认的堆大小,该方法适用于Java 6u20以及之后版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用-server
Yeamin:~ mac$ java -server -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version | grep -i heapsize
uintx ErgoHeapSizeLimit = 0 {product}
uintx HeapSizePerGCThread = 87241520 {product}
uintx InitialHeapSize := 268435456 {product}
uintx LargePageHeapSizeThreshold = 134217728 {product}
uintx MaxHeapSize := 4294967296 {product}
java version "1.8.0_192"
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
# 使用-client
Yeamin:~ mac$ java -client -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version | grep -i heapsize
uintx ErgoHeapSizeLimit = 0 {product}
uintx HeapSizePerGCThread = 87241520 {product}
uintx InitialHeapSize := 268435456 {product}
uintx LargePageHeapSizeThreshold = 134217728 {product}
uintx MaxHeapSize := 4294967296 {product}
java version "1.8.0_192"
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)

查看结果,我们发现,Mac OS中client和server模式的初始化堆大小是268435456(即268435456/1024/1024=256M=物理内存16G \ 1024 \ 1/64)。我们发现实际上这个表中数据是有误的。

其实不仅仅是Mac OS,Windows 64位也是这样的。

总结

  • 堆分配过小,程序大部分时间消耗在GC上。堆分配过大,GC停顿时间加长,停顿频率减少,使得程序性能整天下降。
  • JVM不像是物理内存一样可以使用交互/分页技术。计算机是物理内存和虚拟内存结合使用,程序中运行不活跃的数据复制到虚拟内存(磁盘),而JVM完全没有这种内存交换细节,分配多少G就是多少G。
  • 不同操作系统、不同大小物理内存、不同JVM的默认堆大小有所差异。但是我们常用的都是64位的JVM,常用的初始化堆大小是物理内存的1/64,常用的最大堆大小为物理内存的1/4。
  • 堆的初始化大小和最大堆大小由-Xms:N和-Xmx:N两个参数来控制。其中N表示存储大小,默认为MB为基本单位。
  • 每次GC的时候会动态调整堆的大小。若默认空余堆内存小于40%时,JVM会动态将堆内存增大到-Xmx最大限制。若默认空余堆内存小于40%时,JVM会动态将堆内存减少到-Xms最小限制。因此我们一般设置-Xms和-Xmx相等,避免在每次GC后调整堆的大小。
  • 除非应用程序需要比默认值更大的堆,否则在进行调优时,尽量考虑通过GC算法的性能目标,而非微调堆的大小来改善程序性能。

参考文献

  • 《Java性能权威指南》
支付宝打赏 微信打赏

请作者喝杯咖啡吧