抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

JVM性能调优

JVM 调优概述

性能定义

  • 吞吐量 - 指不考虑 GC 引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。
  • 延迟 - 其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。
  • 内存占用 - 垃圾收集器流畅运行所需要的内存数量

调优原则

GC 优化的两个目标
将进入老年代的对象数量降到最低

​ 除了可以在 JDK7 及更高版本中使用的 G1 收集器以外,其他分代 GC 都是由 Oracle JVM 提供的。关于分代 GC,就是对象在 Eden 区被创建,随后被转移到 Survivor 区,在此之后剩余的对象会被转入老年代。也有一些对象由于占用内存过大,在 Eden 区被创建后会直接被传入老年代。老年代 GC 相对来说会比新生代 GC 更耗时,因此,减少进入老年代的对象数量可以显著降低 Full GC 的频率。你可能会以为减少进入老年代的对象数量意味着把它们留在新生代,事实正好相反,新生代内存的大小是可以调节的。

少 Full GC 的执行时间

Full GC 的执行时间比 Minor GC 要长很多,因此,如果在 Full GC 上花费过多的时间(超过 1s),将可能出现超时错误。

  • 如果通过减小老年代内存来减少 Full GC 时间,可能会引起 OutOfMemoryError 或者导致 Full GC 的频率升高。
  • 另外,如果通过增加老年代内存来降低 Full GC 的频率,Full GC 的时间可能因此增加。

因此,你需要把老年代的大小设置成一个“合适”的值。

GC 优化需要考虑的 JVM 参数

类型 参数 描述
堆内存大小 -Xms 启动 JVM 时堆内存的大小
-Xmx 堆内存最大限制
新生代空间大小 -XX:NewRatio 新生代和老年代的内存比
-XX:NewSize 新生代内存大小
-XX:SurvivorRatio Eden 区和 Survivor 区的内存比

GC 优化时最常用的参数是-Xms,-Xmx-XX:NewRatio-Xms-Xmx参数通常是必须的,所以NewRatio的值将对 GC 性能产生重要的影响。

有些人可能会问如何设置永久代内存大小,你可以用-XX:PermSize-XX:MaxPermSize参数来进行设置,但是要记住,只有当出现OutOfMemoryError错误时你才需要去设置永久代内存。

GC 优化的基本原则是

将不同的 GC 参数应用到两个及以上的服务器上然后比较它们的性能,然后将那些被证明可以提高性能或减少 GC 执行时间的参数应用于最终的工作服务器上。

GC 优化的过程

监控 GC 状态

你需要监控 GC 从而检查系统中运行的 GC 的各种状态。

使用命令监控GC状态

jstat -gcutil $PID(进程ID) $PERIOD(间隔时间,单位毫秒) $TIMES(次数)

分析监控结果后决定是否需要优化 GC

在检查 GC 状态后,你需要分析监控结构并决定是否需要进行 GC 优化。如果分析结果显示运行 GC 的时间只有 0.1-0.3 秒,那么就不需要把时间浪费在 GC 优化上,但如果运行 GC 的时间达到 1-3 秒,甚至大于 10 秒,那么 GC 优化将是很有必要的。

但是,如果你已经分配了大约 10GB 内存给 Java,并且这些内存无法省下,那么就无法进行 GC 优化了。在进行 GC 优化之前,你需要考虑为什么你需要分配这么大的内存空间,如果你分配了 1GB 或 2GB 大小的内存并且出现了OutOfMemoryError,那你就应该执行堆快照(heap dump)来消除导致异常的原因。

堆快照(heap dump)是一个用来检查 Java 内存中的对象和数据的内存文件。该文件可以通过执行 JDK 中的jmap命令来创建。在创建文件的过程中,所有 Java 程序都将暂停,因此,不要在系统执行过程中创建该文件。

设置 GC 类型/内存大小

​ 如果你决定要进行 GC 优化,那么你需要选择一个 GC 类型并且为它设置内存大小。此时如果你有多个服务器,请如上文提到的那样,在每台机器上设置不同的 GC 参数并分析它们的区别。

分析结果

​ 在设置完 GC 参数后就可以开始收集数据,请在收集至少 24 小时后再进行结果分析。如果你足够幸运,你可能会找到系统的最佳 GC 参数。如若不然,你还需要分析输出日志并检查分配的内存,然后需要通过不断调整 GC 类型/内存大小来找到系统的最佳参数。

配置参数

如果结果令人满意,将参数应用到所有服务器上并结束 GC 优化

如果 GC 优化的结果令人满意,就可以将参数应用到所有服务器上,并停止 GC 优化

服务器上进行JVM调优

为了充分利用高性能服务器的硬件资源,有两种JVM调优方案,它们都有各自的优缺点,需要根据具体的情况进行选择。

64为操作系统

采用64位操作系统,并为JVM分配大内存

我们知道,如果JVM中堆内存太小,那么就会频繁地发生垃圾回收,而垃圾回收都会伴随不同程度的程序停顿,因此,如果扩大堆内存的话可以减少垃圾回收的频率,从而避免程序的停顿。

因此,人们自然而然想到扩大内存容量。而32位操作系统理论上最大只支持4G内存,64位操作系统最大能支持128G内存,因此我们可以使用64位操作系统,并使用64位JVM,并为JVM分配更大的堆内存。但问题也随之而来。

堆内存变大后,虽然垃圾收集的频率减少了,但每次垃圾回收的时间变长。如果对内存为14G,那么每次Full GC将长达数十秒。如果Full GC频繁发生,那么对于一个网站来说是无法忍受的。

因此,对于使用大内存的程序来说,一定要减少Full GC的频率,如果每天只有一两次Full GC,而且发生在半夜, 那完全可以接受。

要减少Full GC的频率,就要尽量避免太多对象进入老年代,可以有以下做法:

确保对象都是“朝生夕死”的

一个对象使用完后应尽快让他失效,然后尽快在新生代中被Minor GC回收掉,尽量避免对象在新生代中停留太长时间。

提高大对象直接进入老年代的门槛

通过设置参数-XX:PretrnureSizeThreshold来提高大对象的门槛,尽量让对象都先进入新生代,然后尽快被Minor GC回收掉,而不要直接进入老年代。

使用64位JDK的注意点

64位JDK支持更大的堆内存,但更大的堆内存会导致一次垃圾回收时间过长。

现阶段,64位JDK的性能普遍比32位JDK低。

堆内存过大无法在发生内存溢出时生成内存快照

若将堆内存设为10G,那么当堆内存溢出时就要生成10G的大文件,这基本上是不可能的。

相同程序,64位JDK要比32位JDK消耗更大的内存

使用32位JVM集群

针对于64位JDK种种弊端,我们更多选择使用32位JDK集群来充分利用高性能机器的硬件资源。

实现

在一台服务器上运行多个服务器程序,这些程序都运行在32位的JDK上。然后再运行个服务器作为反向代理服务器,由它来实现负载均衡。

由于32位JDK最多支持2G内存,因此每个虚拟结点的堆内存可以分配1.6G,一共运行10个虚拟结点的话,这台物理服务器可以拥有16G的堆内存。

弊端

多个虚拟节点竞争共享资源时容易出现问题

如多个虚拟节点共同竞争IO操作,很可能会引起IO异常。

很难高效地使用资源池

如果每个虚拟节点使用各自的资源池,那么无法实现各个资源池的负载均衡。如果使用集中式资源池,那么又存在竞争的问题。

每个虚拟节点最大内存为2G

别忘了直接内存也可能导致内存溢出!

问题描述

​ 有个小型网站,使用32位JDK,堆1.6G。运行期间发现老是出现内存溢出。为了判断是否是堆内存溢出,在程序运行前添加参数:-XX:+HeapDumpOnOutOfMemeryError(添加这个参数后当堆内存溢出时就会输出异常日至)。但当再次发生内存溢出时,没有生成相关异常日志。从而可以判定,不是堆内存发生溢出。

问题分析

​ 我们可以发现,在32位JDK中,将1.6G分配给了堆,还有一部分分配给了JVM的其它内存,只有少于0.4G的内存为非JVM内存。我们知道,如果使用了NIO,那么JVM会在JVM内存之外分配内存空间,这部分内存也叫“直接内存”。因此,如果程序中使用了NIO,那么就要小心“直接内存”不足时发生内存溢出异常了!

直接内存的垃圾回收过程

​ 直接内存虽然不是JVM内存空间,但它的垃圾回收也有JVM负责。直接内存的垃圾回收发生在Full GC时,只有当老年代内存满时,垃圾收集器才会顺便收集一下直接内存中的垃圾。

​ 如果直接内存已满,但老年代没满,这时直接内存先是抛出异常,相应的catch块中调用System.gc()。由于System.gc()只是建议JVM回收,JVM可能不马上回收内存,那么这时直接内存就抛出内存溢出异常,使得程序终止。

JVM崩溃的原因

​ 当内存溢出时,JVM仅仅会终止当前运行的程序,那么什么时候JVM会崩溃呢?

什么是异步请求?

​ 我们知道,Web服务器和客户端采用HTTP通信,而HTTP底层采用TCP通信。异步通信就是当客户端向服务器发送一个HTTP请求后,将这个请求的TCP连接委托给其它线程,然后它转而做别的事,那条被委托的线程保持TCP连接,等待服务器的回信。当收到服务器回信后,再将收到的数据转交给刚才的线程。这个过程就是异步通信过程。

异步请求如何造成JVM崩溃?

​ 如果一个Web应用使用了较多的异步请求(AJAX),每次主线程发送完请求后都将TCP连接交给一条新的线程去等待服务器回信,那么如果网络不流畅时,这些受委托的线程迟迟等不到服务器的回信,因此保持着TCP连接。当TCP连接过多时,超过JVM的承受能力,JVM就发生崩溃。

如何处理大对象?

​ 大对象对于JVM来说是个噩耗。如果对象过大,当前新生代的剩余空间装不下它,那么就需要使用分配担保机制,将当前新生代的对象都复制到老年代中,给大对象腾出空间。分配担保涉及到大量的复制,因此效率很低。

​ 那么,如果将大对象直接放入老年代,虽然避免了分配担保过程,但该对象只有当Full GC时才能被回收,而Full GC的代价是高昂的。如果大对象过多时,老年代很快就装满了,这时就需要进行Full GC,如果Full GC频率过高,程序就会变得很卡。

因此,对于大对象,有如下几种处理方法:
  1. 在写程序的时候尽量避免大对象

从源头降低大对象的出现,尽量选择空间利用率较高的数据结构存储。

  1. 尽量缩短大对象的有效时间

对象用完后尽快让它失效,好让垃圾收集器尽快将他回收,避免因在新生代呆的时间过长而进入老年代。

Full GC 应对策略

System.gc()方法的调用

​ 此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

老年代代空间不足

​ 老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space
​ 为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

元空间内存不足

JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永久代或者元空间

​ Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space
​ 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

CMS GC 错误

CMS GC时出现promotion failed和concurrent mode failure

​ 对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。

​ promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。

​ 对措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕

​ 后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

Minor GC晋升问题

统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间

​ 这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。

​ 例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。

​ 当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。

​ 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

堆中分配很大的对象

所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

​ 为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

除了以上6种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

​ 可以利用rmi远程方式在服务器空闲时间进行Full GC,并将这个远程调用的客户端作为定时任务进行远程管理。可以让服务器的内存的可用空间维持在一个稳定的水平,因为在服务器空闲时间进行的,也同时保证了Full GC时的卡顿现象不被用户所感知。

GC 调优方法

目标

​ 满足应用的响应时间和吞吐量需求,尽量减少GC对应用的影响

原则

  1. 大部分时候都不需要调优GC,只需配置-Xms,-Xmx即可,JVM会自动进行调整
  2. 先满足响应时间需求,再满足吞吐量需求
  3. FullGC对应用的影响更大,要尽量减少FullGC执行的时间和频率,减少转移到Old的对象数量

监控GC状态

查看一下GC的总体执行情况

jstat -gcutil pid

参数 说明
YGC Minor GC执行的次数
YGCT Minor GC执行的总耗时
YGCT/YGC Minor GC平均耗时
FGC Full GC执行的次数
FGCT Full GC执行的总耗时
FGCT/FGC Full GC平均耗时

查看一下GC执行的频率和详细情况

  • 在命令行中加入-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename
  • 查看GC日志中每次GC的间隔时间,即可得出GC执行的频率
  • 也可以使用GcViewer工具,导入GC日志,进行更详细的分析(趋势分析,什么时间段GC执行得更频繁,耗时更多等)

分析监控结果

分析GC结果,确定是否需要调优,可参考以下标准(实际标准以应用的实际情况为准)

  • Minor GC平均耗时少于50毫秒
  • Minor GC平均频率少于10秒
  • Full GC平均耗时少于1秒
  • Full GC平均频率少于10分钟

当出现OutOfMemoryError时,要确定是什么问题,是内存分配不足还是因为内存泄漏或者没有及时,可以通过jmap命令或者在命令行加入-XX:+HeapDumpOnOutOfMemoryError,然后对dump出的Heap进行分析(可以使用MAT或者jvisualvm工具)

调优GC策略

基本策略

代空间调整

调整Heap中各区的大小,目标是寻找一个平衡点,在不发生OutOfMemoryError的情况下,满足应用响应时间和吞吐量的需求

  • 增大内存,可以减少GC执行频率,但会增加GC执行时间
  • 减少内存,可以减少GC执行时间,但会增加GC执行频率
内存不作动态调整

内存不作动态调整,因为每次调整内存(Heap或Perm)都会触发FullGC

  • -Xms和-Xmx设置成相同(整个Heap大小不变)
  • 设置-Xmn(Young区大小不变,从而Old区的大小也不变)
  • 设置-XX:SurvivorRatio(Eden、两个Survivor大小不变)
  • -XX:PermSize和-XX:MaxPermSize设置成相同(Perm区大小不变)
永久代和元空间的调整

​ 注意:永久代或者元空间内并没有保存类实例的具体信息(即类对象),也没有反射对象(如方法对象);这些内容都保存在常规的堆空间内。只在编译器或者JVM的运行时有用,这部分信息被称为“类的元数据”。
​ 对永久代而言,可以通过-XX:PermSize=N和-XX:MaxPermSize=N调整大小;而元空间大小可以通过-XX:MetaspaceSize=N和-XX:MaxMetaspaceSize=N调整。

避免主动触发GC
  • 程序调用了System.gc方法,会触发FullGC
  • 在命令行中加入-XX:+DisableExplicitGC,可以忽略主动GC的调用
控制并发

除Serial收集器之外几乎所有的垃圾收集器使用的算法基于多线程。启动的线程数由-XX:ParallelGCThreads=N参数控制。下面的参数可以调整线程的数目:

  • 使用-XX:+UseParallelGC收集新生代空间
  • 使用-XX:+UseParallelOldGC收集老年代空间
  • 使用-XX:+UseParNewGC收集新生代空间
  • 使用-XX:+UseG1GC收集新生代空间
  • CMS收集的“时空停顿”阶段(但并非Full GC)
  • G1收集器的“时空停顿”阶段(但并非Full GC)
自适应调整

根据调优的策略,JVM会不断地尝试,寻找优化性能的机会,所以在JVM的运行过程中,堆、代以及Survivor空间的大小都可能会发生变化。自适应调整在两个方面能提供重要帮助:

A、意味着小型应用程序不需要再为指定了过大的堆而担心。
B、意味着很多应用程序根本不需要担心它们堆的大小,如果需要使用的堆大小超过了平台的默认值,可以放心的 分配更大的堆,而不用关心其他的细节。JVM会自动调整堆和代的大小,依据垃圾回收算法的性能目标,使用优化的内存量。自适应调整就是让自动调整生效的法宝。
使用-XX:-UseAdaptiveSizePolicy可以在全局范围内关闭自适应调整功能。(如果想了解JVM空间如何调整,可以通过-XX:+PrintAdaptiveSizePolicy)

响应时间调优

  1. 使用-XX:+UseParallelOldGC策略
  2. 观察Minor GC的频率和耗时,确定Young大小
    确保只调整Young大小,不调整Old和Perm的大小
    耗时大,则减少Eden,频率高则增加Eden,此时Survivor大小不变
    如果耗时和频率都合适,则保持Eden大小不变,调整Survivor大小,相应要调整Young大小,主要目的是减少从Young移到Old的对象数量
    观察每次GC后是否有对象迁移到Old,如果有,则增加Survivor大小或对象Age大小,确保将对象尽可能地留在Young(设置-XX:+PrintTenuringDistribution,可以看到每次GC后Survivor的大小和各个Age对象的数量,如果Survivor满了,则增加Survivor大小,如果Survivor未满,但仍有对象迁移到Old(排除大对象),则设置age=max(age)+1,-XX:MaxTenuringThreshold=N[0-31])
  3. 观察Full GC的频率和耗时,确定Old大小
    确保只调整Old大小,不调整Young和Perm大小
    耗时大则减少Old,频率高则增加Old
    如果调整后仍然不能满足耗时需求,则考虑使用CMS策略。此时Old要增大20%-30%,因为CMS会产生碎片,要预留更多的内存给应用,否则容易产生concurrent mode error错误,会退化成Serial回收,耗时更大。如果出现concurrent mode error,可以增大Old或者减少碎片整理的百分比(-XX:CMSInitiatingOccupancyFraction=N),增大CMS的频率,加快收集Old

吞吐量调优

​ 在满足响应时间的前提下,增加Young和Old的大小,然后再进行响应时间的分析

推荐配置

基本配置
1
2
3
4
5
6
7
8
9
10
11
12
-Xms=-Xmx
-XX:PermSize=-XX:MaxPermSize
#设置-Xmn
#设置-XX:SurvivorRatio
-Xss=256k(默认是1M,一般不需要那么大,如果发生StackOverflowError,则增加此值)
-XX:-UseAdaptiveSizePolicy(不允许JVM动态调整各区的大小)
-XX:+DisableExplicitGC(关闭主动GC调用)
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:filename
CMS配置
1
2
3
4
5
6
7
8
9
10
11
-XX:+UseConcMarkSweepGC
-XX:+CMSParallelRemarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+ ScavengeBeforeFullGC
-XX+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=X
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSInitiatingPermOccupancyFraction=80
-XX:+CMSClassUnloadingEnabled
-XX:+CMSPermGenSweepingEnabled

评论