JVM性能调优解决方案(总结)一、引言本文的读者是技术支持人员。阅读本文后,你将理解jboss的启动脚本文件(run.sh)中有一系列的JVM配置参数的含义,以及如何调整它们,从而使得MegaEyes中心管理服务器的性能得到优化。MegaEyes中心管理服务器的性能调优涉及到系统的多个方面,包括MegaEyes应用本身、应用服务器(jboss)、数据库和java虚拟机(JVM)等等。本文重点介绍JVM的性能优化。需要注意的是,JVM性能调优具有应用独特性(applicationspecific),就是说,不同的应用情形应该有不同的调整方案,这就要求你首先要观察JVM的运行状态,然后根据观察结果调整参数。没有一个通用的调优方案可以适用于所有的MegaEyes应用。什么是性能调优对性能调优,不同的人有不同的理解,本文是指对下列指标最大化:并发用户(concurrentusers),在服务请求失败或请求响应超过预期时间之前,系统支持的最大并发用户数量。系统容量(throughput),可以用每秒处理的事务(transaction)数量计算。可靠性(reliability)换句话说,我们想对更多的用户提供更快捷的、不会中断的服务。JVM性能调优的重点JVM的性能调优的重点是垃圾回收(gc,garbagecollection)和内存管理。垃圾回收的时候会导致整个虚拟机暂停服务,因此,应该尽可能地缩短垃圾回收的处理时间。JVM内存JVM占用的内存称为堆(heap),它被分为三个区:年轻(young,又称为new)、老(tenured,又称为old)和永生(perm)。这三个区是按照java对象的生存期划分的,在new区的对象生存期最短,很快就会被gc回收;perm区的对象生存期最长,与JVM同生死。Perm区的对象不会被gc回收。new区又被分为三个部分:伊甸园(eden)和两个幸存者(survivor)。对象的创建总是在eden部分(这大概就是命名该部分为eden的原因吧)。两个survivor中总有一个是空的,它作为另一个survivor的缓冲区。当gc发生时,所有eden和survivor中活下来的对象被移动到另一个survivor中。对象会在两个survivor之间不断移动,直到活得足够久,然后移动到old区。我们可以猜想,之所以如此划分使用内存,肯定是为了缩短gc的执行时间,提高gc的执行效率。垃圾回收算法除了默认的垃圾回收算法外,JVM还提供了两个:并行(parallel)和并发(concurrent),前者作用在new区,后者作用在old区。两者可以同时使用。并行算法会产生多个线程以提高执行效率。当有多个cpu的时候,它会显著缩短gc的工作时间。并发算法可以在JVM不中断对应用的服务的情况下执行(通常情况下,在gc工作的时候JVM停止对应用的服务)。二、性能参数参数含义说明-XmsHeap的最小尺寸-XmxHeap的最大尺寸作为一个通行的准则,设置Xms和Xmx的尺寸一样,以减少gc的次数。要将它们设置足够大,否则就会产生outofmemory错误,但又不能设置过大,过大会增加gc的工作时间。-Xmnnew的尺寸-XX:PermSizePerm的最小尺寸-XX:MaxPermSizePerm的最大尺寸类似heap的设置,应该将perm设置为固定尺寸,即最大和最小尺寸一样。-XX:SurvivorRatioNew区中eden与Survivor区的比值-XX:+UseParallelGC使用parallelgc-XX:ParallelGCThreadsParallelgc的线程个数与cpu个数相同,使得所有cpu都参与gc工作。JVM的参数主要由-X和-XX类型的选项组成,上边列出了一些对内存和gc的性能影响比较大的。三、性能参数调优要调整参数首先要观察它们。观察JVM内存和gc的工具很多,jdk本身也提供了一些,这些工具简单、实用,而且不需要安装。其中,最常用的是jps和jstat,前者用来查看JVM的进程id(pid),后者用这个pid作为参数来得到内存和gc的状态,就是说,在执行jstat之前必须用jps得到JVM的pid。Jstat的例子::jstat-gcutil2130825010其中,21308是(运行jboss)的JVM的pid;250是采样间隔,单位是毫秒,即250毫秒采集一次数据;10是采样次数。上述命令的执行结果如下:S0S1EOPYGCYGCTFGCFGCTGCT0.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.0810.000.0011.395.2913.573281.955327133.126135.081列标题含义说明S0SurvivorS1SurvivorEEdenOOldPPerm以上数据都是百分比。YGCYoung(new)区完成的gc的次数YGCTYGC消耗的总时间(秒)FGC整个heap完成的gc的次数如果采用了parallelgc,你会看到YGC明显大于FGC。FGCTFGC消耗的总时间(秒)GCTYGCT+FGCT我们可以将采样次数设置足够大,这样就可以看到内存和gc的变化了。从上述数据可以看出,内存各区域的占用率都不高,gc的执行时间都不长,不过,perm区有些太大,太浪费了。因为perm区的对象与JVM的生命周期是一样的,对象数量不会动态变化,所以,我们可以把这个区域的尺寸设置为原尺寸的二分之一,这样,perm的占用率将从13%左右增加到26%左右。从上述数据还可以看出,new区的gc明显比真个heap的gc快得多。通常,FGC应该不超过400毫秒,否则,将严重影响java应用的正常运行。---------------------------------分割线------------------------------------一、JVM内存模型及垃圾收集算法1.根据Java虚拟机规范,JVM将内存划分为:New(年轻代)Tenured(年老代)永久代(Perm)其中New和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm不属于堆内存,有虚拟机直接分配,但可以通过-XX:PermSize-XX:MaxPermSize等参数调整其大小。年轻代(New):年轻代用来存放JVM刚分配的Java对象年老代(Tenured):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代永久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够,设置原则是预留30%的空间。New又分为几个部分:Eden:Eden用来存放JVM刚分配的对象Survivor1Survivro2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。2.垃圾回收算法垃圾回收算法可以分为三类,都基于标记-清除(复制)算法:Serial算法(单线程)并行算法并发算法JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如,如果机器多于1个核,会对年轻代选择并行算法,关于选择细节请参考JVM调优文档。稍微解释下的是,并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。所以,并发算法适用于交互性高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。还有一个问题是,垃圾回收动作何时执行?当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC当年老代满时会引发FullGC,FullGC将会同时回收年轻代、年老代当永久代满时也会引发FullGC,会导致Class、Method元信息的卸载另一个问题是,何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出JVM98%的时间都花费在内存回收每次回收的内存小于2%满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印HeapDump。二、内存泄漏及解决方法1.系统崩溃前的一些现象:每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5sFullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC年老代的内存越来越大并且每次FullGC后年老代没有内存被释放之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。2.生成堆的dump文件通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。3.分析dump文件下面要考虑的是如何打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux。当然我们可以借助X-Window把Linux上的图形导入到Window。我们考虑用下面几种工具打开该文件:1.VisualVM2.IBMHeapAnalyzer3.JDK自带的Hprof工具使用这些工具时为了确保加载速度,建议设置最大内存为6G。使用后发现,这些工具都无法直观地观察到内存泄漏,VisualVM虽能观察到对象大小,但看不到调用堆栈;HeapAnalyzer虽然能看到调用堆栈,却无法正确打开一个3G的文件。因此,我们又选用了Eclipse专门的静态内存分析工具:Mat。4.分析内存泄漏通过Mat我们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。针对本案,在ThreadLocal中有很多的JbpmContext实例,经过调查是JBPM的Context没有关闭所致。另,通过Mat或JMX我们还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。5.回归问题Q:为什么崩溃前垃圾回收的时间越来越长?A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据Q:为什么FullGC的次数越来越多?A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收Q:为什么年老代占用的内存越来越大?A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代三、性能调优除了上述内存泄漏外,我们还发现CPU长期不足3%,系统吞吐量不够,针对8core