JVM 虚拟机调优思路(1)--理论指导

124 阅读7分钟
1:调整堆大小
选择堆的大小其实是一种平衡。

1.1 如果分配的堆过于小,程序的大部分时间可能都消耗在GC上,没有足够的时间去运行应用程序的逻辑。

1.2 但是,简单粗暴地设置一个特别大的堆也不是解决问题的方法。GC停顿消耗的时间取决于堆的大小,如果增大堆的空间,垃圾回收,尤其是Full GC停顿的持续时间也会变长,造成系统卡顿。这种情况下,停顿的频率会变得更少,但是它们持续的时间会让程序的整体性能变慢。

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

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



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

1.4 即使你显式地设置了堆的最大容量,还是会发生堆的自动调节:初始时堆以默认的大小开始运行,为了达到根据垃圾收集算法设置的性能目标,JVM会逐步增大堆的大小。将堆的大小设置得比实际需要更大不一定会带来性能损耗: 堆并不会无限地增大,JVM会调节堆的大小直到其满足GC的性能目标。
另一方面,如果你确切地了解应用程序需要多大的堆,那么你可以将堆的初始值和最大值直接设置成对应的数值(譬如:-Xms4096n-Xmx4096m)。这种设置能稍微提高GC的运行效率,因为它不再需要估算堆是否需要调整大小了。



2 代空间的调整

一旦堆的大小确定下来,你(或者JVM)就需要决定分配多少堆给新生代空间,多少给老年代空间。我们应该清楚地了解代的划分对性能的影响:如果新生代分配得比较大,垃圾收集发生的频率就比较低,从新生代晋升到老年代的对象也更少。任何事物都有两面性,采用这种分配方法,老年代就相对比较小,比较容易被填满,会更频繁地触发Full GC. 这里找到一个恰当的平衡点是解决问题的关键。


最初新生代空间大小是由NewRatio指定大小,NewRatio的默认值为2。影响堆空间大小的参数通常以比率的方式指定;这个值被用于一个计算空间分配的公式之中。下面是使用NewRatio 计算空间的公式:
Initial Young Gen Size= Initial Heap Size /(1+ NewRatio)
代入堆的初始大小和NewRatio的值就能得到新生代的设置值。那么我们很容易得出,默认情况下,新生代空间的大小是初始堆大小的33%。


如果堆的大小扩张,新生代的大小也会随之增大,直到由MaxNewSize标志设定的最大容量。默认情况下,新生代的最大值也是由 NewRatio 的值设定的,不过它也同时受制于堆的最大容量(注意,不是初始大小)。

试图通过指定新生代的最大及最小值区间的方式调优新生代的结果是十分困难的。如果堆的大小是固定的(可以通过将-xms和-xmx指定为相等的值实现),通常推荐使用-Xmn标志将新生代也设定为固定大小。如果应用程序需要动态调整堆的大小,并希望有一个更大(或者更小)的新生代,那就需要关注NewRatio值的设定。


3 永久代和元空间的调整

由于元空间默认的大小是没有作限制的,因此Java 8(尤其是32位系统)的应用可能由于元空间被填满而耗尽内存。第8章中介绍的工具本地内存跟踪器(Native Memory Tracking,NMT)可以帮助诊断这种类型的问题。如果元空间增长得过大,通过设置MaxMetaspaceSize 你可以调整元空间的上限、将其限制为一个更小的值、不过这又会导致应用程序最后由于元空间耗尽,发生 OutOfMemoryError异常。解决这类问题的终极方法还是定位出为什么类的元空间会变得如此巨大。

堆转储(参见第7章)的信息可以用于诊断存在哪些类加载器,而这些信息反过来可以帮助确定是否存在类加载器的泄漏,最终导致永久代(或者元空间)被耗尽。除此之外,使用jmap和-pernstat参数(适用于Java 7)、或者-clstats参数(适用于Java 8)可以输出类加载器相关的信息。不过这些命令都不是非常稳定,所以不大推荐使用。

4 自适应调整

根据调优的策略,JVM会不断地尝试,寻找优化性能的机会,所以在JVM的运行过程中,堆、栈以及 Survivor空间的大小都可能会发生变化。


自适应调整在两个方面能提供重要的帮助。

其一,这意味着小型应用程序不需要再为指定了过大的堆而担心。譬如用于调整应用服务器的命令行管理程序,这类型的程序通常使用16 MB(或者64 MB)的堆,即使默认的堆可能增长到1 GB那么大的容量。有了自适应调整之后,这种类型的应用程序不再需要额外花费精力去调优,平台默认的配置就能确保他们不会使用大量的内存。


其次,这意味着很多应用程序根本不需要担心它们堆的大小,如果需要使用的堆的大小超过了平台的默认值,他们可以放心地分配更大的堆,而不用关心其他的细节。JVM会自动调整堆和栈的大小,依据垃圾回收算法的性能目标,使用优化的内存量。

自适应调整就是让自动调整生效的法宝。
不过,空间大小的调整终归要花费一定的时间开销,这部分时间大多数消耗在GC停顿的时候。如果你投注了大量的时间精细地调优了垃圾回收的参数、定义了应用程序堆的大小限制,可以考虑关闭自适应调整。如果应用程序的运行明显地可以划分成不同的阶段,你希望对这些阶段中的某一个阶段进行垃圾回收的优化,那么关闭自适应调优也是很有帮助的。