Tomcat性能调优-JVM监控与调优(转)

图片 3

本篇是”GC专家系列“的第三篇。在第一篇理解Java垃圾回收中我们学习了几种不同的GC算法的处理过程,GC的工作方式,新生代与老年代的区别。所以,你应该已经了解了JDK
7中的5种GC类型,以及每种GC对性能的影响。

参数设置

图片 1

在第二篇Java垃圾回收的监控中介绍了在真实场景中JVM是如何运行GC,如何监控GC数据以及有哪些工具可用来方便进行GC监控。

在Java虚拟机的参数中,有3种表示方法用“ps -ef |grep
“java”命令,可以得到当前Java进程的所有启动参数和配置参数:

1、GC相关的参数:

-Xms / -Xmx — 堆的初始大小 / 堆的最大大小

-Xmn — 堆中年轻代的大小

-XX:-DisableExplicitGC — 让System.gc()不产生任何作用

-XX:+PrintGCDetails — 打印GC的细节

-XX:+PrintGCDateStamps — 打印GC操作的时间戳

-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小

-XX:NewRatio — 可以设置老生代和新生代的比例

-XX:PrintTenuringDistribution —
设置每次新生代GC后输出幸存者乐园中对象年龄的分布

-XX:InitialTenuringThreshold /
-XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值

-XX:TargetSurvivorRatio:设置幸存区的目标使用率

在本篇中,我将基于真实的案例来介绍一些GC调优的最佳选项。写本篇文章时,我假设你已经理解了前两篇的内容。为了深入理解本部分内容,你最好先浏览一下前两篇的内容——如果你尚未了解的话。

标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;

2、GC回收器类型

       
JVM中的垃圾收集一般都采用“分代收集”,不同的堆内存区域采用不同的收集算法,主要目的就是为了增加吞吐量或降低停顿时间。

Serial收集器:新生代收集器,使用复制算法,使用一个线程进行GC,串行,其它工作线程暂停。

ParNew收集器:新生代收集器,使用复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial
Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。

Parallel Scavenge 收集器

       
吞吐量优先的垃圾回收器,作用在新生代,使用复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间。使用-XX:+UseParallelGC开关控制使用Parallel
Scavenge+Serial Old收集器组合回收垃圾。

Serial Old收集器

       
老年代收集器,单线程收集器,串行,使用标记整理算法,使用单线程进行GC,其它工作线程暂停。

Parallel Old收集器

       
吞吐量优先的垃圾回收器,作用在老年代,多线程,并行,多线程机制与Parallel
Scavenge差不错,使用标记整理算法,在Parallel
Old执行时,仍然需要暂停其它线程。

CMS(Concurrent Mark Sweep)收集器

     
 老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial
Old进行内存回收,优先使用ParNew+CMS(原因见Full
GC和并发垃圾回收一节),当用户线程内存不足时,采用备用方案Serial
Old收集。

GC调优是必须的吗

更精确的说, 基于Java的服务是否一定需要GC调优
?应该说,GC调优并非所有Java服务都必须做的事情。当然这是基于你已经使用了下面的选项或事实:

  • 通过 -Xms 和 -Xmx 选项指定了内存大小
  • 使用了 -server 选项
  • 系统未产生太多超时日志

非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;

3、Full GC

       
并发垃圾回收器的内存回收过程是与用户线程一起并发执行的。通常情况下,并发垃圾回收器可以在用户线程运行的情况下完成大部分的回收工作,所以应用停顿时间很短。

       
但由于并发垃圾回收时用户线程还在运行,所以会有新的垃圾不断产生。作为担保,如果在老年代内存都被占用之前,如果并发垃圾回收器还没结束工作,那么应用会暂停,在所有用户线程停止的情况下完成回收。这种情况称作Full
GC,这意味着需要调整有关并发回收的参数了。

        由于Full
GC很影响应用的性能,要尽量避免或减少。特别是如果对于高容量低延迟的电商系统,要尽量避免在交易时间段发生Full
GC。

也就是说,如果你未设置内存大小并且你的系统产生了过多的超时日志,恭喜你需要为你的系统执行GC调优。

但是,请记住: GC调优是不得已时的选择

思考一下GC调优的深层原因。垃圾回收器会去清理Java中创建的对象。GC需要清理的对象数据以及GC执行的次数取决于应用创建对象的多少。因此,为了控制GC的执行,首先你需要
减少对象的创建

俗话说“积重难返”。所以我们需要从小处着手,否则它们将不断壮大直到难以管理。

  • 应该多使用 StringBuilder 和 StringBuffer 对象替代 String 。
  • 减少不必要的日志输出。

即便如此,面对有些场景我们依然无能为力。我们知道解析XML和JSON会占用大量的内存空间。即便我们尽可能少的使用
String
,尽可能好的优化日志输出,然而在解析XML和JSON时仍然会有大量的内存开销,甚至有10~100MB之多,可我们很难杜绝XML和JSON的使用。但是请记住:XML和JSON会带来很大的内存开销。

如果应用的内存占用不断提升,你就要开始对其进行GC调优了。我把GC调优的目标分为以下两类:

  • 降低移动到老年代的对象数量
  • 缩短Full GC的执行时间

非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用(但是,这些参数往往是非常有用的);

4、总结

(1)为了分代垃圾回收,Java堆内存分为3代:新生代,老年代和永久代。

(2)新的对象实例会优先分配在新生代,在经历几次Minor
GC后(默认15次),还存活的会被移至老年代(某些大对象会直接在老年代分配)。

(3)永久代是否执行GC,取决于采用的JVM。Java中不能手动触发GC,但可以用不同的引用类来辅助垃圾回收器工作(比如:弱引用或软引用)。

(4)Minor GC发生在新生代,当Eden区没有足够空间时,会发起一次Minor
GC,将Eden区中的存活对象移至Survivor区。Major
GC发生在老年代,当升到老年代的对象大于老年代剩余空间时会发生Major GC。

(5)发生Major GC时,用户线程会暂停,会降低系统性能和吞吐量。

(6)JVM中Java堆内存的初始大小和最大值,依据个人经验这个值的比例最好是1:1或者1:1.5。比如,你可以将-Xmx和-Xms都设为1GB,或者-Xmx和-Xms设为1.8GB和1.2GB。

参考文献:https://yemengying.com/2016/05/13/jvm-GC/

降低移动到老年代的对象数量

在Oracle JVM中除了JDK 7及最高版本中引入的G1
GC外,其他的GC都是基于分代回收的。也就是对象会在Eden区中创建,然后不断在Survivor中来回移动。之后如果该对象依然存活,就会被移到老年代中。有些对象,因为占用空间太大以致于在Eden区中创建后就直接移动到了老年代。老年代的GC较新生代会耗时更长,因此减少移动到老年代的对象数量可以降低full
GC的频率。减少对象转移到老年代可能会被误解为把对象保留在新生代,然而这是不可能的,相反你可以
调整新生代的空间大小

(额外的,-DpropertyName=“value”的形式定义了一些全局属性值,下面有介绍。)

缩短Full GC耗时

Full GC的单次执行与Minor GC相比,耗时有较明显的增加。如果执行Full
GC占用太长时间(例如超过1秒),在对外服务的连接中就可能会出现超时。

  • 如果企图通过缩小老年代空间的方式来降低Full GC执行时间,可能会面临
    OutOfMemoryError 或者带来更频繁的Full GC。
  • 如果通过增加老年代空间来减少Full GC执行次数,单次Full
    GC耗时将会增加。

因此,需要 为老年代空间设置适当的大小

本文只重点介绍一些重要和常用的参数,如果想了解全部参数,可以参考下面的文章:

影响GC性能的选项

在理解Java垃圾回收的结尾,我说过不要有这样的想法:
别人通过某个GC选项获得了明显的性能提升,为什么我不直接用这个选项呢
。因为 不同的服务所拥有的对象数量和对象的生命周期是不同的

一个简单场景,如果执行一个任务需要五个条件:A, B, C,
D和E,另外一个任务只需要两个条件A和B,哪个任务会快一些?通常只需要条件A和B的任务会快一些。

Java
GC选项的设置也是一样的道理。设置很多选项未必能提高GC执行速度,相反还可能会更加耗时。
GC调优的基本规则是对两台或更多的服务器设置不同的选项,并对比性能表现
,然后把被证明能提升性能的选项添加到应用服务器上。请记住这一点。

下表列出了与内存相关的且会影响性能的GC选项:

《Java HotSpot VM
Options》

表1: GC调优需要关注的选项

分类 选项 说明
堆空间 -Xms 启动JVM时的初始堆空间大小
-Xmx 堆空间最大值
新生代空间 -XX:NewRatio 新生代与老年代的比例
-XX:NewSize 新生代大小
-XX:SurvivorRatio Eden区与Survivor区的比例

我经常会使用的选项是: -Xms , -Xmx 和 -XX:NewRatio ,其中 -Xms 和 -Xmx
是必须的。而如何设置 -XX:NewRatio 对性能会有显著的影响。

可能有人会问 如何设置永久代(Perm)的大小 , 可以使用 -XX:PermSize 和
-XX:MaxPermSize 进行设置,但记住只有发生由Perm空间不足导致的
OutOfMemoryError 时才需要设置。

另外一个会影响GC性能的选项是GC类型,下表列出了JDK
6.0中能使用的相关设置选项:

《Java 6
JVM参数选项大全(中文版)》(上面一篇的中文版)

表2: GC类型选项

分类

选项

说明

Serial GC

-XX:+UseSerialGC

Parallel GC

-XX:+UseParallelGC-XX:ParallelGCThreads=<value>

Parallel Compacting GC

-XX:+UseParallelOldGC

CMS GC

-XX:+UseConcMarkSweepGC
-XX:UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=<value>
-XX:+UseCMSInitiatingOccupancyOnly

G1

-XX:+UnlockExperimentalVMOptions-XX:+UseG1GC

在JDK6中使用G1时,这两个选项必须同时设置

除了G1,其他GC类型都是通过每个选行列的第一行选项进行设置。通常最不会使用的是Serial
GC,它是为client应用优化和设计的。

还有很多其他影响GC性能的选项,但不如上面这些对性能的影响明显。另外设置更多选项未必能优化GC的执行时间。

《JVM启动参数大全》

GC调优过程

GC调优过程与一般的性能改进流程很相似,下面会介绍我在GC调优过程中的流程。

标准参数

1. 监控GC状态

首先需要监控GC状态信息以明确在GC操作过程中对系统的影响。具体方式可以回顾上一篇文章:Java
垃圾回收的监控。

其实标准参数是用过Java的人都最熟悉的,就是你在运行java命令时后面加上的参数,如java
-version, java -jar 等,输入命令java -help或java
-?就能获得当前机器所有java的标准参数列表。

2. 分析监控数据并决定是否需要GC调优

然后通过GC操作状态,对监控结果进行分析,并判断是否有必要进行GC调优。如果分析结果显示GC耗时在0.1-0.3秒以内的话,一般不需要花费额外的时间做GC调优。然而,
如果GC耗时达到1-3秒甚至10秒以上,就需要立即对系统进行GC调优 。

但是如果你的应用分配了10GB的内存,且不能降低内存容量的话,其实是没办法进行GC调优的。这种情况下,你首先要去思考为什么需要分配这么大的内存。如果只给应用分配了1GB或者2GB内存,当有
OutOfMemeoryError
发生时,你需要通过堆dump来分析验证内存溢出的原因并进行修复。

注释:堆dump是把内存情况按一定格式输出到文件,可用于检查Java
内存中的对象和数据情况。可使用JDK中内置的 jmap
命令创建堆dump文件。创建文件过程中,Java进程会中断,因此不要在正常运行时系统上做此操作。

-client

3. 设置GC类型和内存大小

如果决定做GC调优,就需要考虑如何选择GC类型、如何设置内存大小。如果你有多台服务器,可通过为每台服务器设置不同的GC选项并对比不同的表现,这一步很重要。

设置jvm使用client模式,这是一般在pc机器上使用的模式,启动很快,但性能和内存管理效率并不高;多用于桌面应用;

4. 分析GC调优结果

设置GC选项后,至少要收集24小时的GC表现数据,然后就可以着手分析这些数据了。如果足够幸运,通过分析就刚好找到了最合适的GC选项。否则就需要分析GC日志,并分析内存的分配情况。然后通过不同的调整GC类型和内存大小来找到系统的最优选项。

-server

5. 如果结果可接受,则对所有服务应用调优选项并停止调优

如果GC结果令人满意,就可以把相应的选项应用到所有服务器并停止GC调优。

下面的章节会详细介绍每个步骤中的详细过程。

使用server模式,启动速度虽然慢(比client模式慢10%左右),但是性能和内存管理效率很高,适用于服务器,用于生成环境、开发环境或测试环境的服务端;

监控GC状态并分析GC结果

监控Web应用(WAS: Web Application Server)GC运行状态的最好方式是使用
jstat 命令。在Java
垃圾回收的监控部分已经介绍了如何使用jstat命令,所以这里就直接介绍怎么样来校验结果数据。

下面的例子中列出了JVM未做GC调优时的数据:

$ jstat -gcutil 21719 1s
S0    S1    E    O    P    YGC    YGCT    FGC    FGCT GCT
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673

看一下表中的YGC和YGCT,YGCT 除以
YGC算出平均单次YGC耗时为0.05秒。也就是说在新生代执行一次垃圾回收的平均耗时为50毫秒。通过这份结果,我们可以无须关注新生代的垃圾回收。

然后再看一下FGCT和FGC,FGCT除以FGC算出平均单次FGC耗时为19.68秒。也就是平均需要消耗19.68秒来执行一次Full
GC。上面的结果(共3次Full GC)可能是每次Full
GC都耗时19.68秒,也有可能是其中两次都只耗时1秒,而另外一次却消耗了58秒。然而不管哪种情况,都迫切需要进行GC调优。

当然也可以通过 jstat 来校验结果,不过分析GC的最好方式是使用
-verbosegc
选项来启动JVM。在前面的文章中我已经详细介绍了生成日志的方式以及如何进行分析。就分析
-verbosegc 日志而言, HPJMeter
是我最偏爱的工具,因为它简单易用。使用HPJMeter可以轻松获取GC执行时间的开销以及GC发生的频率。

如果GC执行时间满足以下判断条件,那么GC调优并没那么必须。

  • Minor GC执行迅速(50毫秒以内)
  • Minor GC执行不频繁(间隔10秒左右一次)
  • Full GC执行迅速(1秒以内)
  • Full GC执行不频繁(间隔10分钟左右一次)

括号内的值并非绝对,依据应用的服务状态会有不同。有些服务可能要求Full
GC处理速度不能超过0.9秒,另外一些服务可能会宽松些。因此校验GC结果并根据具体的服务需要,决定是否要进行GC调优。

在校验GC状态时,不要只关心Minor GC和Full GC的耗时,也要
GC执行次数也同样重要 。如果新生代太小,Minor
GC就会频繁执行(甚至每间隔1秒就要执行一次)。另外,新生代太小导致转移到老年代的对象增多,也会引起Full
GC的频繁执行。因此使用`-gccapacity`配合jstat命令,以检查内存空间的使用情况。

如果没有指定-server或-client,JVM启动的时候会自动检测当前主机是否为服务器,如果是就以server模式启动,64位的JVM只有server模式,所以无法使用-client参数;

设置GC类型和内存大小

默认情况下,不同的启动模式,执行GC的方式有所区别:

设置GC类型

Oracle JVM提供了5种GC类型,如果是低于JDK 7的版本,可以使用Parallel GC,
Parallel Compacting GC, CMS
GC。当然,到底选哪一个并没有统一的准则或标准。

所以 如何选择合适的GC类型
?推荐方案是将这三种GC都应用到应用中进行对比。不过可以明确的是CMS
GC肯定比Parallel GCs更快,即然这样只使用CMS GC便好。然而CMS
GC也有出问题的时候,通常Full GC中使用CMS GC会执行更快,如果CMS
GC的并发模式失败,则会出现比Parallel GCs慢的情况。

启动模式新生代GC方式旧生代和持久代GC的方式

并发模式失败

我们来深入看一下并发模式失败的场景。

Parallel GC与CMS
GC最大的区别在于压缩任务。压缩任务通过压缩内存使用来移除内存中的碎片空间,以清理两块已分配使用的内存空间中的间隙。

在Parallel GC中,只要执行Full GC便会进行内存压缩,因此耗时更长。不过Full
GC之后,因为压缩的原故,可以分配连续的空间,所以内存的分配速度为更快一些。

与之相反,CMS
GC的执行中并不会伴随内存压缩,因此GC速度会更快一些。然而,因此未做内存压缩,
GC清理过程中释放的内存便会成为空闲空间。因为空间不连续,可能会导致在创建大对象时空间不足。例如,如果老年代尚有300M空闲,却不能为10MB的对象分配足够的连续空间。这时便会发生
并发模式失败 的警告,并触发内存压缩。如果使用CMS
GC,在内存压缩过程中可能会比Parallel
GCs更为耗时,也可能会带来其他问题。关于”并发模式失败”更详细的介绍可以看Oracle
工程师的文章: 理解CMS GC
日志

结论就是,要为你的系统寻找合适的GC类型。

每个系统都有一个最适当的GC类型,所以你需要找到这个GC类型。如果你有6台服务器,建议你为每两组设置相同的选项,并通过
-verbosegc 选项对结果进行分析和比较。

client串行串行

调整内存大小

下面先列出内存大小与GC执行次数、每次GC耗时之间的关系:

  • 大内存
    • 会降低GC执行次数
    • 相应的会增加GC执行耗时
  • 小内存
    • 会缩知单次GC耗时
    • 相应的会增加GC执行次数

当然,关于使用大内存还是小内存并没有唯一正确的答案。如果服务器资源足够且Full
GC执行耗时能控制在1秒以内,使用10GB的内存也是可以的。但大多数时候如果设置内存为10GB,GC执行效果并不尽人意,执行一次Full
GC可能要消耗10~30秒(具体时长也会根据对象大小情况而不同)。

既然如此, 如何正确设置内存大小
。通常情况下,我会推荐500MB大小。这不是说你要把自己的WAS(Web Application
Server)内存选项设置为 -Xms500 和 -Xmx500m
。基于当前未调优时的场景,检查Full GC之后内存大小变化。如果Full
GC之后尚有300MB空间剩余,这样最好把内存设置到1GB(300MB(默认使用) +
500MB(老年代最小容量) +
200MB(空闲空间))。这意味着你应该才老年代至少设置500MB空间。如果你有3台服务器,可以分别设置1GB、1.5GB和2GB,并检查每台机器的执行结果。

理论上,根据内存大小不同单次执行GC速度应该是1GB > 1.5GB >
2GB,所以1GB的内存会中三个之中GC速度最快的。但并不能保证1GB的内存Full
GC耗时1秒,2GB的内存Full
GC耗时2秒。实际耗时与机器性能和对象大小也有关系。所以最好的度量方式是设置每种可能性并分析他们的监控结果。

有设置内存大小时,还需要设置另外一选项: NewRatio 。 NewRatio
是新生代与老年代的比值的倒数(即老年代与新生代的比值)。如果 XX:NewRatio=1
,就是说新生代 :
老年代的比值为1:1。对于1GB内存,就是新生代与老年代各500MB。如果 NewRatio
的值是2,则是新生代 :
老年代的值为1:2。因此比值设置的越大,老年代的空间就越大,相应的新生代空间会越小。

设置 NewRatio
也不是一件重要的事,但可能会对整个GC性能带来严重影响。如果新生代太小,对象就会转移到老年代,引起频繁的Full
GC,导致更多的耗时。

你可能简单的认为设置 NewRatio=1 会带来最佳的效果,然而并非如此。把
NewRatio
设置为2或3更容易带来好的GC表现。当然我也实际遇到过一些这样的例子。

完成GC调优的最快途径是什么?通过对比性能测试的结果是得到GC调优结果的最快途径。通过为每个服务器设置不同的选项并观察GC状态,最好能观察1到2天的数据。如果是通过性能测试来做GC调优的话,要为每个服务器准备相同的负载和业务操作。请求比例的分配也要与业务条件相一致。然而即便是专业的性能测试人员,准备精确的负载数据也并非易事,通常需要花费很大精力来做准备。所以更简捷的GC调优方式就是对业务应用准备GC选项,然后通过等待GC结果并进行分析,尽管可能需要更长的等待时间。

server并行并发

分析GC调优结果

在应用GC选项并设置 -verbosegc 后,可以通过 tail
命令检查日志是否按期望的方式正常输出。如果选项未精确的设置或者没有按期望输出,你所花费的时间都将白费。如果日志输出与期望相符,等待1到2天的运行后便可检查和分析结果。最简单的方式是把日志文件复制到本地PC,并使用
HPJMeter 进行分析。

分析过程中主要关注以下数据,下面列表是按我自己定义的优先级列出的。其中决定GC选项的最重要的数据是Full
GC执行时间。

  • Full GC(平均)耗时
  • Minor GC(平均)耗时
  • Full GC执行间隔
  • MinorGC执行间隔
  • Full GC整体耗时
  • Minor GC整体耗时
  • GC整体耗时
  • Full GC执行次数
  • Minor GC执行次数

如果足够幸运,你能恰好找到合适的GC选项,通常你并没这么幸运。执行GC调优时一定要格外小心,因为如果你试图一次就完成GC调优,得到的可能会是
OutOfMemoryError 。

如果没有指定-server或-client模式,则判断方法如下:

调优案例

上面我们对于GC调优的讨论还仅是纸上谈兵,现在开始我们看一些具体的GC调优的案例。

案例1

这个例子是为服务S进行的GC优化。对于这个新上线的服务S,在执行Full
GC时有些过于耗时。

先看一下 jstat -gcutil 的结果:

S0 S1 E O P YGC YGCT FGC FGCT GCT
12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993

在开始进行调优时不用太关心 持久代
空间的设置,相对而言YGC的数值更值得关注。

从上面的结果中我们可算出执行Minor GC和Full
GC的平均时间上的开销,如下表:

-classpath / -cp

表3:服务S执行Minor GC和Full GC的平均耗时

GC类型 GC 执行次数 GC执行时间 平均耗时
Minor GC 54 2.047 37 ms
Full GC 5 6.946 1389 ms

对于Minor GC来说, 37 ms 还不算坏,而Full GC的平均耗时 1.389 s
对于系统来说在执行Full GC时可能会导致频繁的超时现象,例如DB超时设置为1
s的话就会发生超时。所以这个案例中的系统需要进行GC调优。

首先在开始GC调优之前先检查当前的内存设置。可以使用 jstat -gccapacity
选项查看内存的使用情况。下面是服务S的检查结果:

NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC
212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5

其中关键的数据如下:

  • 新生代使用:212, 992 KB(约208 MB)
  • 老年代使用:1,884,160 KB(约1.8 GB)

所以除去持久代之外的内存分配为2 GB,且新生代 : 老年代为 1:9 (即
NewRatio=9 )。为了看到更详细的信息,对系统的三个不同实现均设置了
-verbosegc 并分别设置了 NewRatio 选项,除此之外未添加其他选项。

  • NewRatio = 2
  • NewRatio = 3
  • NewRatio = 4

一天之后检查GC时日志时幸运的发生,在设置 NewRatio 之后尚未有Full
GC发生。

发生了什么?因为大多数对象在创建之后不久就被销毁,所以新生代里的对象在移到老年代之前就被销毁掉了。

既然如此,就没必要再设置其他选项,只是选择好最佳的 NewRatio 即可。
如何选取最佳NewRatio ?只能逐个分析设置不同 NewRatio 值时的Minor
GC的平均耗时。

上面三个 NewRatio 设置对应的Minor GC平均耗时如下:

  • NewRatio=2: 45ms
  • NewRatio=3: 34ms
  • NewRatio=4: 30ms

因为 NewRatio=4 时Minor
GC具有最小的耗时,所以就是我们选择的最佳设置,即便此时新生代的空间相对较小。应用此选项后,服务再也没有Full
GC发生。

下面是系统重新设置过选项后,某天通过 jstat -gcutil 查看到的结果:

S0 S1 E O P YGC YGCT FGC FGCT GCT
8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219

你可能认为因为系统接收的请求太少以致于GC发生频率较低,然而在Minor
GC执行了2,424次的情况下系统未发生Full GC。

JVM加载和搜索文件的目录路径,多个路径用;分隔。注意,如果使用了-classpath,JVM就不会再搜索环境变量中定义的CLASSPATH路径。

案例2

下面介绍的是服务A的例子。我们在公司的应用性能管理平台(APM: Application
Performance
Manager)上发现服务A的JVM周期性的出现长时间的停顿(超过8秒未有响应)的现象。所以我们决定对其进行GC调优。经过排查我们发现此系统在执行Full
GC时太过耗时,需要进行优化。

在着手优化之前,我们为系统加上了 -verbosegc 选项,输出结果如下图:

图片 2

图1:GC调优之前的GC耗时

上图是HPJMeter自动分析结果后提供的系统GC随着JVM运行的耗时图。 X-轴
是JVM从启动后的运行时间轴, Y-轴
是每次GC的响应时间。其中绿色的是Full
GC使用的CMS垃圾回收的耗时,蓝色的是Minor GC使用的Parallel
Scavenge垃圾回收的耗时。

前面我说过CMS GC是最快的,但上图可看到有场景耗时竟达到15秒之多。
什么原因导致这种后果?
回想一下我前面说过的:当内存压缩时CMS将会变慢。另外服务A设置了 -Xms1g 和
-Xmx4g 的选项,操作系统为其分配的内存为4 GB。

然后我把GC类型由GMS换成了Parallel GC,并把内存大小设置为2G, NewRatio
设置为3。一段时间之后通过 jstat -gcutil 查看到的结果如下:

S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890

Full
GC的速度提升了,与4GB内存时的15秒相比,现在平均每次只需要3秒。但3秒仍然不尽人意,所以我设计了以下六组选项:

  • -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2
  • -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3
  • -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3
  • -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2
  • -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3
  • -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3

哪一个会更快呢?结果显示内存越小,速度越快。下图是第六组选项的GC持续时长分布图,代表了最优的GC性能提升。图中看到最慢的为1.7秒,而平均值降低到1秒以内。

图片 3

图2:使用第六组选项后的GC耗时

因此我把服务A的GC选项调整为了第六组中的设置,然而每天夜里却连续发生了
OutOfMemoryError
。个中艰辛不再细说,简而言之就是批量的数据处理任务导致了JVM内存泄露。到此为止,所有的问题都明了了。

如果只对GC日志做短时间的观察例把GC调优的结果应用到所有服务器上是一件非常危险的事情。一定要记住,如果GC调优能够顺利执行而无故障只有一条途径:像分析GC日志一样分析系统的每一个服务操作。

上面通过两个GC调优的案例演示了GC调优的具体处理过程。如我所述,案例中的GC选项可以不做调整的应用到那些具有相同CPU、操作系统和
JDK
版本以及执行相同功能的服务上去。然而不要把这些选项应用到你的系统上,因为他们未必适用。

JVM搜索路径的顺序为:

总结

我执行GC调优一般基于经验而无需通过堆dump后对内存进行详细的分析,尽管精确的内存状态可能会带来更好的GC调优结果。在一般情景,如果内存负载较低时,通过分析内存对象可能效果更好,不过如果服务负载较高,内存空间使用较多时,更推荐基于经验来做GC调优。

我曾经在一些服务上对G1 GC做过性能测试,不过还没有全面使用。结果证明G1
GC执行速度比其他任何GC都要快,不过需要把JDK升级到 JDK 7
才能享受到G1带来的性能提升,另外G1的稳定性目前尚不能完全保证,没有人知道是否会带来严重的bug。所以大范围使用
G1 还尚待时日。

当 JDK 7 稳定以后(并不是说它当前不稳定),并且WAS针对JDK
7做过优化之后,G1也许会稳定的运行在服务器上,到那时也许就不再需要进行GC调优了。

更多GC调优的细节可以在 Slideshare
上搜索相关材料。我最推荐的是Twitter 工程师 Attila Szegedi写的这篇
我在Twitter学到的关于JVM调优的一切
,有时间可以学习一下。

作者:Sangmin Lee, 性能实验室高级工程师,NHN公司

1,先搜索JVM自带的jar或zip包(Bootstrat,搜索路径可以用System.getProperty(“sun.boot.class.path”)获得);

2,搜索JRE_HOME/lib/ext下的jar包(Extension,搜索路径可以用System.getProperty(“java.ext.dirs”)获得);

3,搜索用户自定义目录,顺序为:当前目录(.),CLASSPATH,-cp;(搜索路径用System.getProperty(“java.class.path”)获得)

-DpropertyName=value

定义系统的全局属性值,如配置文件地址等,如果value有空格,可以用-Dname=”space
string”这样的形式来定义,用System.getProperty(“propertyName”)可以获得这些定义的属性值,在代码中也可以用System.setProperty(“propertyName”,”value”)的形式来定义属性。

-verbose

这是查询GC问题最常用的命令之一,具体参数如:

-verbose:class

输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。

-verbose:gc

输出每次GC的相关情况,后面会有更详细的介绍。

-verbose:jni

输出native方法调用的相关情况,一般用于诊断jni调用错误信息。

非标准参数

非标准参数,是在标准参数的基础上进行扩展的参数,输入“java
-X”命令,能够获得当前JVM支持的所有非标准参数列表(你会发现,其实并不多哦)。

在不同类型的JVM中,采用的参数有所不同,

在讲解非标准参数时,请参考下面的图,对内存区域的大小有个形象的了解(下图出自:

-Xmn

新生代内存大小的最大值,包括E区和两个S区的总和,使用方法如:-Xmn65535,-Xmn1024k,-Xmn512m,-Xmn1g
(-Xms,-Xmx也是种写法)

-Xmn只能使用在JDK1.4或之后的版本中,(之前的1.3/1.4版本中,可使用-XX:NewSize设置年轻代大小,用-XX:MaxNewSize设置年轻代最大值);

如果同时设置了-Xmn和-XX:NewSize,-XX:MaxNewSize,则谁设置在后面,谁就生效;如果同时设置了-XX:NewSize
-XX:MaxNewSize与-XX:NewRatio则实际生效的值是:min(MaxNewSize,max(NewSize,
heap/(NewRatio+1)))(看考:

在开发、测试环境,可以-XX:NewSize 和
-XX:MaxNewSize来设置新生代大小,但在线上生产环境,使用-Xmn一个即可(推荐),或者将-XX:NewSize

-XX:MaxNewSize设置为同一个值,这样能够防止在每次GC之后都要调整堆的大小(即:抖动,抖动会严重影响性能)

-Xms

初始堆的大小,也是堆大小的最小值,默认值是总共的物理内存/64(且小于1G),默认情况下,当堆中可用内存小于40%(这个值可以用-XX:
MinHeapFreeRatio
调整,如-X:MinHeapFreeRatio=30)时,堆内存会开始增加,一直增加到-Xmx的大小;

-Xmx

堆的最大值,默认值是总共的物理内存/64(且小于1G),如果Xms和Xmx都不设置,则两者大小会相同,默认情况下,当堆中可用内存大于70%(这个值可以用-XX:
MaxHeapFreeRatio
调整,如-X:MaxHeapFreeRatio=60)时,堆内存会开始减少,一直减小到-Xms的大小;

整个堆的大小=年轻代大小+年老代大小,堆的大小不包含持久代大小,如果增大了年轻代,年老代相应就会减小,官方默认的配置为年老代大小/年轻代大小=2/1左右(使用-XX:NewRatio可以设置-XX:NewRatio=5,表示年老代/年轻代=5/1);

建议在开发测试环境可以用Xms和Xmx分别设置最小值最大值,但是在线上生产环境,Xms和Xmx设置的值必须一样,原因与年轻代一样——防止抖动;

-Xss

这个参数用于设置每个线程的栈内存,默认1M,一般来说是不需要改的。除非代码不多,可以设置的小点,另外一个相似的参数是-XX:ThreadStackSize,这两个参数在1.6以前,都是谁设置在后面,谁就生效;1.6版本以后,-Xss设置在后面,则以-Xss为准,-XXThreadStackSize设置在后面,则主线程以-Xss为准,其它线程以-XX:ThreadStackSize为准。

-Xrs

减少JVM对操作系统信号(OS
Signals)的使用(JDK1.3.1之后才有效),当此参数被设置之后,jvm将不接收控制台的控制handler,以防止与在后台以服务形式运行的JVM冲突(这个用的比较少,参考:

-Xprof

跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。

-Xnoclassgc

关闭针对class的gc功能;因为其阻止内存回收,所以可能会导致OutOfMemoryError错误,慎用;

-Xincgc

开启增量gc(默认为关闭);这有助于减少长时间GC时应用程序出现的停顿;但由于可能和应用程序并发执行,所以会降低CPU对应用的处理能力。

-Xloggc:file

与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。

若与verbose命令同时出现在命令行中,则以-Xloggc为准。

非Stable参数(非静态参数)

以-XX表示的非Stable参数,虽然在官方文档中是不确定的,不健壮的,各个公司的实现也各有不同,但往往非常实用,所以这部分参数对于GC非常重要。JVM(Hotspot)中主要的参数可以大致分为3类(参考

性能参数( Performance
Options):用于JVM的性能调优和内存分配控制,如初始化内存大小的设置;

行为参数(Behavioral
Options):用于改变JVM的基础行为,如GC的方式和算法的选择;

调试参数(Debugging
Options):用于监控、打印、输出等jvm参数,用于显示jvm更加详细的信息;

比较详细的非Stable参数总结,请参考Java 6
JVM参数选项大全(中文版),

对于非Stable参数,使用方法有4种:

-XX:+ 启用选项

-XX:- 不启用选项

-XX:= 给选项设置一个数字类型值,可跟单位,例如 32k, 1024m, 2g

-XX:= 给选项设置一个字符串值,例如-XX:HeapDumpPath=./dump.core

首先介绍性能参数,性能参数往往用来定义内存分配的大小和比例,相比于行为参数和调试参数,一个比较明显的区别是性能参数后面往往跟的有数值,常用如下:

参数及其默认值描述

-XX:NewSize=2.125m

新生代对象生成时占用内存的默认值

-XX:MaxNewSize=size新生成对象能占用内存的最大值

-XX:MaxPermSize=64m方法区所能占用的最大内存(非堆内存)

-XX:PermSize=64m方法区分配的初始内存

-XX:MaxTenuringThreshold=15

对象在新生代存活区切换的次数(坚持过MinorGC的次数,每坚持过一次,该值就增加1),大于该值会进入老年代

-XX:MaxHeapFreeRatio=70

GC后java堆中空闲量占的最大比例,大于该值,则堆内存会减少

-XX:MinHeapFreeRatio=40GC后java堆中空闲量占的最小比例,小于该值,则堆内存会增加

-XX:NewRatio=2新生代内存容量与老生代内存容量的比例

-XX:ReservedCodeCacheSize= 32m保留代码占用的内存容量

-XX:ThreadStackSize=512设置线程栈大小,若为0则使用系统默认值

-XX:LargePageSizeInBytes=4m设置用于Java堆的大页面尺寸

-XX:PretenureSizeThreshold=
size大于该值的对象直接晋升入老年代(这种对象少用为好)

-XX:SurvivorRatio=8Eden区域Survivor区的容量比值,如默认值为8,代表Eden:Survivor1:Survivor2=8:1:1

常用的行为参数,主要用来选择使用什么样的垃圾收集器组合,以及控制运行过程中的GC策略等:

参数及其默认值描述

-XX:-UseSerialGC

启用串行GC,即采用Serial+Serial Old模式

-XX:-UseParallelGC

启用并行GC,即采用Parallel Scavenge+Serial
Old收集器组合(-Server模式下的默认组合)

-XX:GCTimeRatio=99设置用户执行时间占总时间的比例(默认值99,即1%的时间用于GC)

-XX:MaxGCPauseMillis=time设置GC的最大停顿时间(这个参数只对Parallel
Scavenge有效)

-XX:+UseParNewGC使用ParNew+Serial Old收集器组合

-XX:ParallelGCThreads设置执行内存回收的线程数,在+UseParNewGC的情况下使用

-XX:+UseParallelOldGC

使用Parallel Scavenge +Parallel Old组合收集器

-XX:+UseConcMarkSweepGC使用ParNew+CMS+Serial
Old组合并发收集,优先使用ParNew+CMS,当用户线程内存不足时,采用备用方案Serial
Old收集。

-XX:-DisableExplicitGC禁止调用System.gc();但jvm的gc仍然有效

-XX:+ScavengeBeforeFullGC新生代GC优先于Full GC执行

常用的调试参数,主要用于监控和打印GC的信息:

参数及其默认值描述

-XX:-CITime打印消耗在JIT编译的时间

-XX:ErrorFile=./hs_err_pid.log保存错误日志或者数据到文件中

-XX:-ExtendedDTraceProbes开启solaris特有的dtrace探针

-XX:HeapDumpPath=./java_pid.hprof指定导出堆信息时的路径或文件名

-XX:-HeapDumpOnOutOfMemoryError当首次遭遇OOM时导出此时堆中相关信息

-XX:OnError=”;”出现致命ERROR之后运行自定义命令

-XX:OnOutOfMemoryError=”;”当首次遭遇OOM时执行自定义命令

-XX:-PrintClassHistogram遇到Ctrl-Break后打印类实例的柱状信息,与jmap
-histo功能相同

-XX:-PrintConcurrentLocks遇到Ctrl-Break后打印并发锁的相关信息,与jstack
-l功能相同

-XX:-PrintCommandLineFlags打印在命令行中出现过的标记

-XX:-PrintCompilation当一个方法被编译时打印相关信息

-XX:-PrintGC每次GC时打印相关信息

-XX:-PrintGC Details每次GC时打印详细信息

-XX:-PrintGCTimeStamps打印每次GC的时间戳

-XX:-TraceClassLoading跟踪类的加载信息

-XX:-TraceClassLoadingPreorder跟踪被引用到的所有类的加载信息

-XX:-TraceClassResolution跟踪常量池

-XX:-TraceClassUnloading跟踪类的卸载信息

-XX:-TraceLoaderConstraints跟踪类加载器约束的相关信息

再次声明,上面的三种参数,主要参考了博客:

这些参数将为我们进行GC的监控与调优提供很大助力,是我们进行GC相关操作的重要工具。

收集器搭配

在介绍了常用的配置参数之后,我们将开始真正的JVM实操征程,首先,我们要为应用程序选择一个合适的垃圾收集器组合,本节请参考《[Java系列笔记(3)

  • Java
    内存区域和GC机制](

这里需要再次引用这幅图(图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用):

Serial收集器:Serial收集器是在client模式下默认的新生代收集器,其收集效率大约是100M左右的内存需要几十到100多毫秒;在client模式下,收集桌面应用的内存垃圾,基本上不影响用户体验。所以,一般的Java桌面应用中,直接使用Serial收集器(不需要配置参数,用默认即可)。

ParNew收集器:Serial收集器的多线程版本,这种收集器默认开通的线程数与CPU数量相同,-XX:ParallelGCThreads可以用来设置开通的线程数。

可以与CMS收集器配合使用,事实上用-XX:+UseConcMarkSweepGC选择使用CMS收集器时,默认使用的就是ParNew收集器,所以不需要额外设置-XX:+UseParNewGC,设置了也不会冲突,因为会将ParNew+Serial
Old作为一个备选方案;

如果单独使用-XX:+UseParNewGC参数,则选择的是ParNew+Serial
Old收集器组合收集器。

一般情况下,在server模式下,如果选择CMS收集器,则优先选择ParNew收集器。

Parallel
Scavenge收集器:
关注的是吞吐量(关于吞吐量的含义见上一篇博客),可以这么理解,关注吞吐量,意味着强调任务更快的完成,而如CMS等关注停顿时间短的收集器,强调的是用户交互体验。

在需要关注吞吐量的场合,比如数据运算服务器等,就可以使用Parallel
Scavenge收集器。

老年代收集器如下:

Serial Old收集器:在1.5版本及以前可以与 Parallel
Scavenge结合使用(事实上,也是当时Parallel
Scavenge唯一能用的版本),另外就是在使用CMS收集器时的备用方案,发生
Concurrent Mode Failure时使用。

如果是单独使用,Serial Old一般用在client模式中。

Parallel Old收集器:在1.6版本之后,与 Parallel
Scavenge结合使用,以更好的贯彻吞吐量优先的思想,如果是关注吞吐量的服务器,建议使用Parallel
Scavenge + Parallel Old 收集器。

CMS收集器:这是当前阶段使用很广的一种收集器,国内很多大的互联网公司线上服务器都使用这种垃圾收集器(

CMSIncrementalMode:CMS收集器变种,属增量式垃圾收集器,在并发标记和并发清理时交替运行垃圾收集器和用户线程。

G1 收集器:面向服务器端应用的垃圾收集器,计划未来替代CMS收集器。

一般来说,如果是Java桌面应用,建议采用Serial+Serial
Old收集器组合,即:-XX:+UseSerialGC(-client下的默认参数)

在开发/测试环境,可以采用默认参数,即采用Parallel Scavenge+Serial
Old收集器组合,即:-XX:+UseParallelGC(-server下的默认参数)

在线上运算优先的环境,建议采用Parallel Scavenge+Serial
Old收集器组合,即:-XX:+UseParallelGC

在线上服务响应优先的环境,建议采用ParNew+CMS+Serial
Old收集器组合,即:-XX:+UseConcMarkSweepGC

另外在选择了垃圾收集器组合之后,还要配置一些辅助参数,以保证收集器可以更好的工作。关于这些参数,请在

选用了ParNew收集器,你可能需要配置4个参数: -XX:SurvivorRatio,
-XX:PretenureSizeThreshold,
-XX:+HandlePromotionFailure,-XX:MaxTenuringThreshold;

选用了 Parallel Scavenge收集器,你可能需要配置3个参数:
-XX:MaxGCPauseMillis,-XX:GCTimeRatio, -XX:+UseAdaptiveSizePolicy ;

选用了CMS收集器,你可能需要配置3个参数:
-XX:CMSInitiatingOccupancyFraction, -XX:+UseCMSCompactAtFullCollection,
-XX:CMSFullGCsBeforeCompaction;

启动内存分配

关于GC有一个常见的疑问是,在启动时,我的内存如何分配?经过前面的学习,已经很容易知道,用-Xmn,-Xmx,-Xms,-Xss,-XX:NewSize,-XX:MaxNewSize,-XX:MaxPermSize,-XX:PermSize,-XX:SurvivorRatio,-XX:PretenureSizeThreshold,-XX:MaxTenuringThreshold就基本可以配置内存启动时的分配情况。但是,具体配置多少?设置小了,频繁GC(甚至内存溢出),设置大了,内存浪费。结合前面对于内存区域和其作用的学习,尽量考虑如下建议:

-XX:PermSize尽量比-XX:MaxPermSize小,-XX:MaxPermSize>= 2 *
-XX:PermSize, -XX:PermSize>
64m,一般对于4G内存的机器,-XX:MaxPermSize不会超过256m;

-Xms = 
-Xmx(线上Server模式),以防止抖动,大小受操作系统和内存大小限制,如果是32位系统,则一般-Xms设置为1g-2g(假设有4g内存),在64位系统上,没有限制,不过一般为机器最大内存的一半左右;

-Xmn,在开发环境下,可以用-XX:NewSize和-XX:MaxNewSize来设置新生代的大小(-XX:NewSize<=-XX:MaxNewSize),在生产环境,建议只设置-Xmn,一般-Xmn的大小是-Xms的1/2左右,不要设置的过大或过小,过大导致老年代变小,频繁Full
GC,过小导致minor
GC频繁。如果不设置-Xmn,可以采用-XX:NewRatio=2来设置,也是一样的效果;

-Xss一般是不需要改的,默认值即可。

-XX:SurvivorRatio一般设置8-10左右,推荐设置为10,也即:Survivor区的大小是Eden区的1/10,一般来说,普通的Java程序应用,一次minorGC后,至少98%-99%的对象,都会消亡,所以,survivor区设置为Eden区的1/10左右,能使Survivor区容纳下10-20次的minor
GC才满,然后再进入老年代,这个与
-XX:MaxTenuringThreshold的默认值15次也相匹配的。如果XX:SurvivorRatio设置的太小,会导致本来能通过minor回收掉的对象提前进入老年代,产生不必要的full
gc;如果XX:SurvivorRatio设置的太大,会导致Eden区相应的被压缩。

-XX:MaxTenuringThreshold默认为15,也就是说,经过15次Survivor轮换(即15次minor
GC),就进入老年代,
如果设置的小的话,则年轻代对象在survivor中存活的时间减小,提前进入年老代,对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率。需要注意的是,设置了
-XX:MaxTenuringThreshold,并不代表着,对象一定在年轻代存活15次才被晋升进入老年代,它只是一个最大值,事实上,存在一个动态计算机制,计算每次晋入老年代的阈值,取阈值和MaxTenuringThreshold中较小的一个为准。

-XX:PretenureSizeThreshold一般采用默认值即可。

监控工具和方法

在JVM运行的过程中,为保证其稳定、高效,或在出现GC问题时分析问题原因,我们需要对GC进行监控。所谓监控,其实就是分析清楚当前GC的情况。其目的是鉴别JVM是否在高效的进行垃圾回收,以及有没有必要进行调优。

通过监控GC,我们可以搞清楚很多问题,如:

1,minor GC和full GC的频率;

2,执行一次GC所消耗的时间;

3,新生代的对象何时被移到老生代以及花费了多少时间;

4,每次GC中,其它线程暂停(Stop the world)的时间;

5,每次GC的效果如何,是否不理想;

………………

监控GC的工具分为2种:命令行工具和图形工具;

常用的命令行工具有:

注:下面的命令都在JAVA_HOME/bin中,是java自带的命令。如果您发现无法使用,请直接进入Java安装目录调用或者先设置Java的环境变量,一个简单的办法为:直接运行命令
export
PATH=$JAVA_HOME/bin:$PATH;另外,一般的,在Linux下,下面的命令需要sudo权限,在windows下,部分命令的部分选项不能使用。

1,jps

jps命令用于查询正在运行的JVM进程,常用的参数为:

-q:只输出LVMID,省略主类的名称

-m:输出虚拟机进程启动时传给主类main()函数的参数

-l:输出主类的全类名,如果进程执行的是Jar包,输出Jar路径

-v:输出虚拟机进程启动时JVM参数

命令格式:jps [option] [hostid]

一个简单的例子:

在上图中,有一个vid为309的apache进程在提供web服务。

2,jstat

jstat可以实时显示本地或远程JVM进程中类装载、内存、垃圾收集、JIT编译等数据(如果要显示远程JVM信息,需要远程主机开启RMI支持)。如果在服务启动时没有指定启动参数-verbose:gc,则可以用jstat实时查看gc情况。

jstat有如下选项:

-class:监视类装载、卸载数量、总空间及类装载所耗费的时间

-gc:监听Java堆状况,包括Eden区、两个Survivor区、老年代、永久代等的容量,以用空间、GC时间合计等信息

-gccapacity:监视内容与-gc基本相同,但输出主要关注java堆各个区域使用到的最大和最小空间

-gcutil:监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比

-gccause:与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因

-gcnew:监视新生代GC状况

-gcnewcapacity:监视内同与-gcnew基本相同,输出主要关注使用到的最大和最小空间

-gcold:监视老年代GC情况

-gcoldcapacity:监视内同与-gcold基本相同,输出主要关注使用到的最大和最小空间

-gcpermcapacity:输出永久代使用到最大和最小空间

-compiler:输出JIT编译器编译过的方法、耗时等信息

-printcompilation:输出已经被JIT编译的方法

命令格式:jstat [option vmid [interval[s|ms] [count]]]

jstat可以监控远程机器,命令格式中VMID和LVMID特别说明:如果是本地虚拟机进程,VMID和LVMID是一致的,如果是远程虚拟机进程,那么VMID格式是:
[protocol:][//]lvmid[@hostname[:port]/servername],如果省略interval和count,则只查询一次

查看gc情况的例子:

在图中,命令sudo jstat -gc 309 1000
5代表着:搜集vid为309的java进程的整体gc状态,
每1000ms收集一次,共收集5次;XXXC表示该区容量,XXXU表示该区使用量,各列解释如下:

S0C:S0区容量(S1区相同,略)

S0U:S0区已使用

EC:E区容量

EU:E区已使用

OC:老年代容量

OU:老年代已使用

PC:Perm容量

PU:Perm区已使用

YGC:Young GC(Minor GC)次数

YGCT:Young GC总耗时

FGC:Full GC次数

FGCT:Full GC总耗时

GCT:GC总耗时

用gcutil查看内存的例子:

图中的各列与用gc参数时基本一致,不同的是这里显示的是已占用的百分比,如S0为86.53,代表着S0区已使用了86.53%

3,jinfo

用于查询当前运行这的JVM属性和参数的值。

jinfo可以使用如下选项:

-flag:显示未被显示指定的参数的系统默认值

-flag [+|-]name或-flag name=value: 修改部分参数

-sysprops:打印虚拟机进程的System.getProperties()

命令格式:jinfo [option] pid

4,jmap

用于显示当前Java堆和永久代的详细信息(如当前使用的收集器,当前的空间使用率等)

-dump:生成java堆转储快照

-heap:显示java堆详细信息(只在Linux/Solaris下有效)

-F:当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照(只在Linux/Solaris下有效)

-finalizerinfo:显示在F-Queue中等待Finalizer线程执行finalize方法的对象(只在Linux/Solaris下有效)

-histo:显示堆中对象统计信息

-permstat:以ClassLoader为统计口径显示永久代内存状态(只在Linux/Solaris下有效)

命令格式:jmap [option] vmid

其中前面3个参数最重要,如:

查看对详细信息:sudo jmap -heap 309

生成dump文件: sudo jmap -dump:file=./test.prof 309

部分用户没有权限时,采用admin用户:sudo -u admin -H  jmap
-dump:format=b,file=文件名.hprof pid

查看当前堆中对象统计信息:sudo  jmap -histo
309:该命令显示3列,分别为对象数量,对象大小,对象名称,通过该命令可以查看是否内存中有大对象;

有的用户可能没有jmap权限:sudo -u admin -H jmap -histo 309 | less

5,jhat

用于分析使用jmap生成的dump文件,是JDK自带的工具,使用方法为: jhat -J
-Xmx512m [file]

不过jhat没有mat好用,推荐使用mat(Eclipse插件:
),mat速度更快,而且是图形界面。

6,jstack

用于生成当前JVM的所有线程快照,线程快照是虚拟机每一条线程正在执行的方法,目的是定位线程出现长时间停顿的原因。

-F:当正常输出的请求不被响应时,强制输出线程堆栈

-l:除堆栈外,显示关于锁的附加信息

-m:如果调用到本地方法的话,可以显示C/C++的堆栈

命令格式:jstack [option] vmid

7,-verbosegc

-verbosegc是一个比较重要的启动参数,记录每次gc的日志,下面的表格对比了jstat和-verbosegc:

jstat-verbosegc

监控对象

运行在本机的Java应用可以把日志输出到终端上,或者借助jstatd命令通过网络连接远程的Java应用。

只有那些把-verbogc作为启动参数的JVM。

输出信息

堆状态(已用空间,最大限制,GC执行次数/时间,等等)

执行GC前后新生代和老年代空间大小,GC执行时间。

输出时间

Every designated time

每次设定好的时间。每次GC发生的时候。

用途观察堆空间变化情况

了解单次GC产生的效果。

与-verbosegc配合使用的一些常用参数为:

-XX:+PrintGCDetails,打印GC信息,这是-verbosegc默认开启的选项

-XX:+PrintGCTimeStamps,打印每次GC的时间戳

-XX:+PrintHeapAtGC:每次GC时,打印堆信息

-XX:+PrintGCDateStamps (from JDK 6 update 4)
:打印GC日期,适合于长期运行的服务器

-Xloggc:/home/admin/logs/gc.log:制定打印信息的记录的日志位置

每条verbosegc打印出的gc日志,都类似于下面的格式:

time [GC [: -> (total occupancy1), secs] -> (total occupancy3),
secs]

如:

这些选项的意义是:

time:执行GC的时间,需要添加-XX:+PrintGCDateStamps参数才有;

collector:minor gc使用的收集器的名字。

starting occupancy1:GC执行前新生代空间大小。

ending occupancy1:GC执行后新生代空间大小。

total occupancy1:新生代总大小

pause time1:因为执行minor GC,Java应用暂停的时间。

starting occupancy3:GC执行前堆区域总大小

ending occupancy3:GC执行后堆区域总大小

total occupancy3:堆区总大小

pause time3:Java应用由于执行堆空间GC(包括full GC)而停止的时间。

8,可视化工具

监控和分析GC也有一些可视化工具,比较常见的有JConsole和VisualVM,有兴趣的可以看看下面的文章,在此不再赘述:

http://blog.csdn.net/java2000_wl/article/details/8049707

调优方法

一切都是为了这一步,调优,在调优之前,我们需要记住下面的原则:

多数的Java应用不需要在服务器上进行GC优化;

多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;

在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);

减少创建对象的数量;

减少使用全局变量和大对象;

GC优化是到最后不得已才采用的手段;

在实际使用中,分析GC情况优化代码比优化GC参数要多得多;

GC优化的目的有两个(

将转移到老年代的对象数量降低到最小;

减少full GC的执行时间;

为了达到上面的目的,一般地,你需要做的事情有:

减少使用全局变量和大对象;

调整新生代的大小到最合适;

设置老年代的大小为最合适;

选择合适的GC收集器;

在上面的4条方法中,用了几个“合适”,那究竟什么才算合适,一般的,请参考上面“收集器搭配”和“启动内存分配”两节中的建议。但这些建议不是万能的,需要根据您的机器和应用情况进行发展和变化,实际操作中,可以将两台机器分别设置成不同的GC参数,并且进行对比,选用那些确实提高了性能或减少了GC时间的参数。

真正熟练的使用GC调优,是建立在多次进行GC监控和调优的实战经验上的,进行监控和调优的一般步骤为:

1,监控GC的状态

使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化;

2,分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化;

注:如果满足下面的指标,则一般不需要进行GC:

Minor GC执行时间不到50ms;

Minor GC执行不频繁,约10秒一次;

Full GC执行时间不到1s;

Full GC执行频率不算频繁,不低于10分钟1次;

3,调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择;

4,不断的分析和调整

通过不断的试验和试错,分析并找到最合适的参数

5,全面应用参数

如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。

调优实例

上面的内容都是纸上谈兵,下面我们以一些真实例子来进行说明:

实例1:

笔者昨日发现部分开发测试机器出现异常:java.lang.OutOfMemoryError: GC
overhead limit
exceeded,这个异常代表:GC为了释放很小的空间却耗费了太多的时间,其原因一般有两个:1,堆太小,2,有死循环或大对象;

笔者首先排除了第2个原因,因为这个应用同时是在线上运行的,如果有问题,早就挂了。所以怀疑是这台机器中堆设置太小;

使用ps -ef |grep “java”查看,发现:

该应用的堆区设置只有768m,而机器内存有2g,机器上只跑这一个java应用,没有其他需要占用内存的地方。另外,这个应用比较大,需要占用的内存也比较多;

笔者通过上面的情况判断,只需要改变堆中各区域的大小设置即可,于是改成下面的情况:

跟踪运行情况发现,相关异常没有再出现;

实例2:

一个服务系统,经常出现卡顿,分析原因,发现Full GC时间太长:

jstat -gcutil:

S0     S1    E     O       P        YGC YGCT FGC FGCT  GCT

12.16 0.00 5.18 63.78 20.32  54   2.047 5     6.946  8.993

分析上面的数据,发现Young GC执行了54次,耗时2.047秒,每次Young
GC耗时37ms,在正常范围,而Full
GC执行了5次,耗时6.946秒,每次平均1.389s,数据显示出来的问题是:Full
GC耗时较长,分析该系统的是指发现,NewRatio=9,也就是说,新生代和老生代大小之比为1:9,这就是问题的原因:

1,新生代太小,导致对象提前进入老年代,触发老年代发生Full GC;

2,老年代较大,进行Full GC时耗时较大;

优化的方法是调整NewRatio的值,调整到4,发现Full GC没有再发生,只有Young
GC在执行。这就是把对象控制在新生代就清理掉,没有进入老年代(这种做法对一些应用是很有用的,但并不是对所有应用都要这么做)

实例3:

一应用在性能测试过程中,发现内存占用率很高,Full GC频繁,使用sudo -u
admin -H  jmap -dump:format=b,file=文件名.hprof pid
来dump内存,生成dump文件,并使用Eclipse下的mat差距进行分析,发现:

从图中可以看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,导致整个线程占用内存高达378m,此时通知开发人员进行代码优化,将相关对象释放掉即可。

参考资料

原文:

《深入理解Java虚拟机:JVM高级特效与最佳实现》

JVM启动参数大全,

JVM系列三:JVM参数设置、分析,

Java 6 JVM参数选项大全(中文版),

成为JavaGC专家Part II — 如何监控Java垃圾回收机制,

成为Java GC专家系列(3) — 如何优化Java垃圾回收机制,

JDK5.0垃圾收集优化之–Don’t Pause,

Java HOTSPOT VM参数大全,

【原】GC的默认方式,

JAVA启动参数大全之三:非Stable参数,

Java虚拟机学习 – 内存调优,

内存溢出,

如何查看JVM的扩展参数:-X,

JVM内存状况查看方法和分析工具,

虚拟机学习系列 – 附 – 虚拟机参数,

JVM系列四:生产环境参数实例及分析【生产环境实例增加中】,

垃圾收集器与内存分配策略,

JVM垃圾收集器使用调查:CMS最受欢迎 ,

Xms Xmx PermSize MaxPermSize 区别,

Java虚拟机学习 – JDK可视化监控工具,

虚拟机学习系列 – 6 – JDK工具,

JVM监控工具介绍jstack, jconsole, jinfo, jmap, jdb, jstat,

JVM 与 jstat,

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图