JVM性能调优解决方案.docx

上传人:b****5 文档编号:4398882 上传时间:2022-12-01 格式:DOCX 页数:10 大小:23.55KB
下载 相关 举报
JVM性能调优解决方案.docx_第1页
第1页 / 共10页
JVM性能调优解决方案.docx_第2页
第2页 / 共10页
JVM性能调优解决方案.docx_第3页
第3页 / 共10页
JVM性能调优解决方案.docx_第4页
第4页 / 共10页
JVM性能调优解决方案.docx_第5页
第5页 / 共10页
点击查看更多>>
下载资源
资源描述

JVM性能调优解决方案.docx

《JVM性能调优解决方案.docx》由会员分享,可在线阅读,更多相关《JVM性能调优解决方案.docx(10页珍藏版)》请在冰豆网上搜索。

JVM性能调优解决方案.docx

JVM性能调优解决方案

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停止对应用的服务)。

二、性能参数

参数

含义

说明

-Xms

Heap的最小尺寸

 

-Xmx

Heap的最大尺寸

作为一个通行的准则,设置Xms和Xmx的尺寸一样,以减少gc的次数。

要将它们设置足够大,否则就会产生outofmemory错误,但又不能设置过大,过大会增加gc的工作时间。

-Xmn

new的尺寸

 

-XX:

PermSize

Perm的最小尺寸

 

-XX:

MaxPermSize

Perm的最大尺寸

类似heap的设置,应该将perm设置为固定尺寸,即最大和最小尺寸一样。

-XX:

SurvivorRatio

New区中eden与Survivor区的比值

 

-XX:

+UseParallelGC

使用parallelgc

 

-XX:

ParallelGCThreads

Parallelgc的线程个数

与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是采样次数。

上述命令的执行结果如下:

S0S1EOPYGCYGCTFGCFGCTGCT

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

0.000.0011.395.2913.573281.955327133.126135.081

 

列标题

含义

说明

S0

Survivor

 

S1

Survivor

 

E

Eden

 

O

Old

 

P

Perm

以上数据都是百分比。

YGC

Young(new)区完成的gc的次数

 

YGCT

YGC消耗的总时间(秒)

 

FGC

整个heap完成的gc的次数

如果采用了parallelgc,你会看到YGC明显大于FGC。

FGCT

FGC消耗的总时间(秒)

 

GCT

YGCT+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刚分配的对象

∙Survivor1

∙Survivro2:

两个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、5s

∙FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC

∙年老代的内存越来越大并且每次FullGC后年老代没有内存被释放

 之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。

 2.生成堆的dump文件

 通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

 3.分析dump文件

 下面要考虑的是如何打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux。

当然我们可以借助X-Window把Linux上的图形导入到Window。

我们考虑用下面几种工具打开该文件:

1.VisualVM

2.IBMHeapAnalyzer

3.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×16G、64bit的Linux服务器来说,是严重的资源浪费。

 在CPU负载不足的同时,偶尔会有用户反映请求的时间过长,我们意识到必须对程序及JVM进行调优。

从以下几个方面进行:

∙线程池:

解决用户响应时间长的问题

∙连接池

∙JVM启动参数:

调整各代的内存比例和垃圾回收算法,提高吞吐量

∙程序算法:

改进程序逻辑算法提高性能

 1.Java线程池(java.util.concurrent.ThreadPoolExecutor)

   大多数JVM6上的应用采用的线程池都是JDK自带的线程池,之所以把成熟的Java线程池进行罗嗦说明,是因为该线程池的行为与我们想象的有点出入。

Java线程池有几个重要的配置参数:

∙corePoolSize:

核心线程数(最新线程数)

∙maximumPoolSize:

最大线程数,超过这个数量的任务会被拒绝,用户可以通过RejectedExecutionHandler接口自定义处理方式

∙keepAliveTime:

线程保持活动的时间

∙workQueue:

工作队列,存放执行的任务

   Java线程池需要传入一个Queue参数(workQueue)用来存放执行的任务,而对Queue的不同选择,线程池有完全不同的行为:

∙SynchronousQueue:

一个无容量的等待队列,一个线程的insert操作必须等待另一线程的remove操作,采用这个Queue线程池将会为每个任务分配一个新线程

∙LinkedBlockingQueue:

无界队列,采用该Queue,线程池将忽略maximumPoolSize参数,仅用corePoolSize的线程处理所有的任务,未处理的任务便在LinkedBlockingQueue中排队

∙ArrayBlockingQueue:

有界队列,在有界队列和maximumPoolSize的作用下,程序将很难被调优:

更大的Queue和小的maximumPoolSize将导致CPU的低负载;小的Queue和大的池,Queue就没起动应有的作用。

   其实我们的要求很简单,希望线程池能跟连接池一样,能设置最小线程数、最大线程数,当最小数<任务<最大数时,应该分配新的线程处理;当任务>最大数时,应该等待有空闲线程再处理该任务。

   但线程池的设计思路是,任务应该放到Queue中,当Queue放不下时再考虑用新线程处理,如果Queue满且无法派生新线程,就拒绝该任务。

设计导致“先放等执行”、“放不下再执行”、“拒绝不等待”。

所以,根据不同的Queue参数,要提高吞吐量不能一味地增大maximumPoolSize。

   当然,要达到我们的目标,必须对线程池进行一定的封装,幸运的是ThreadPoolExecutor中留了足够的自定义接口以帮助我们达到目标。

我们封装的方式是:

∙以SynchronousQueue作为参数,使maximumPoolSize发挥作用,以防止线程被无限制的分配,同时可以通过提高maximumPoolSize来提高系统吞吐量

∙自定义一个RejectedExecutionHandler,当线程数超过maximumPoolSize时进行处理,处理方式为隔一段时间检查线程池是否可以执行新Task,如果可以把拒绝的Task重新放入到线程池,检查的时间依赖keepAliveTime的大小。

 2.连接池(mons.dbcp.BasicDataSource)

   在使用mons.dbcp.BasicDataSource的时候,因为之前采用了默认配置,所以当访问量大时,通过JMX观察到很多Tomcat线程都阻塞在BasicDataSource使用的ApacheObjectPool的锁上,直接原因当时是因为BasicDataSource连接池的最大连接数设置的太小,默认的BasicDataSource配置,仅使用8个最大连接。

   我还观察到一个问题,当较长的时间不访问系统,比如2天,DB上的Mysql会断掉所以的连接,导致连接池中缓存的连接不能用。

为了解决这些问题,我们充分研究了BasicDataSource,发现了一些优化的点:

∙Mysql默认支持100个链接,所以每个连接池的配置要根据集群中的机器数进行,如有2台服务器,可每个设置为60

∙initialSize:

参数是一直打开的连接数

∙minEvictableIdleTimeMillis:

该参数设置每个连接的空闲时间,超过这个时间连接将被关闭

∙timeBetweenEvictionRunsMillis:

后台线程的运行周期,用来检测过期连接

∙maxActive:

最大能分配的连接数

∙maxIdle:

最大空闲数,当连接使用完毕后发现连接数大于maxIdle,连接将被直接关闭。

只有initialSize

这个参数主要用来在峰值访问时提高吞吐量。

∙initialSize是如何保持的?

经过研究代码发现,BasicDataSource会关闭所有超期的连接,然后再打开initialSize数量的连接,这个特性与minEvictableIdleTimeMillis、timeBetweenEvictionRunsMillis一起保证了所有超期的initialSize连接都会被重新连接,从而避免了Mysql长时间无动作会断掉连接的问题。

 3.JVM参数

   在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。

通过设置我们希望达到一些目标:

∙GC的时间足够的小

∙GC的次数足够的少

∙发生FullGC的周期足够的长

 前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。

  

(1)针对JVM堆的设置一般,可以通过-Xms-Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值

  

(2)年轻代和年老代将根据默认的比例(1:

2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过-XX:

newSize-XX:

MaxNewSize来设置其绝对大小。

同样,为了防止年轻代的堆收缩,我们通常会把-XX:

newSize-XX:

MaxNewSize设置为同样大小

  (3)年轻代和年老代设置多大才算合理?

这个我问题毫无疑问是没有答案的,否则也就不会有调优。

我们观察一下二者大小变化有哪些影响

∙更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的FullGC

∙更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少FullGC的频率

∙如何选择应该依赖应用程序对象生命周期的分布情况:

如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。

但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:

(A)本着FullGC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:

2也是这个道理(B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响FullGC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:

1。

但应该给年老代至少预留1/3的增长空间

 (4)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法:

-XX:

+UseParallelOldGC,默认为Serial收集

 (5)线程堆栈的设置:

每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。

理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

 (4)可以通过下面的参数打HeapDump信息

∙-XX:

HeapDumpPath

∙-XX:

+PrintGCDetails

∙-XX:

+PrintGCTimeStamps

∙-Xloggc:

/usr/aaa/dump/heap_trace.txt

   通过下面参数可以控制OutOfMemoryError时打印堆的信息

∙-XX:

+HeapDumpOnOutOfMemoryError

 请看一下一个时间的Java参数配置:

(服务器:

Linux64Bit,8Core×16G)

 JAVA_OPTS="$JAVA_OPTS-server-Xms3G-Xmx3G-Xss256k-XX:

PermSize=128m-XX:

MaxPermSize=128m-XX:

+UseParallelOldGC-XX:

+HeapDumpOnOutOfMemoryError-XX:

HeapDumpPath=/usr/aaa/dump-XX:

+PrintGCDetails-XX:

+PrintGCTimeStamps-Xloggc:

/usr/aaa/dump/heap_trace.txt-XX:

NewSize=1G-XX:

MaxNewSize=1G"

经过观察该配置非常稳定,每次普通GC的时间在10ms左右,FullGC基本不发生,或隔很长很长的时间才发生一次

通过分析dump文件可以发现,每个1小时都会发生一次FullGC,经过多方求证,只要在JVM中开启了JMX服务,JMX将会1小时执行一次FullGC以清除引用,关于这点请参考附件文档。

 4.程序算法调优:

本次不作为重点

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高中教育 > 英语

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1