你了解JVM吗?
你用过分布式吗?
今天我们就来讲讲Java分布式以及性能调优。 其实说到对JVM进行性能调优早已是一个老生常谈的话题,如果你所在的技术团队还暂时达不到淘宝团队那样的高度,无法满足在OpenJDK的基础之上根据自身业务进行针对性的二次开发和定制调优,那么对于你来说,唯一的选择就是尽可能的熟悉JVM的内存布局,以及熟练掌握与GC相关的那些选项配置,否则JVM的基础性能调优不是痴人说梦?
(阅读完大概需要10分钟,学海无涯)
目录
一、性能调优的一些概念和目标;
二、性能调优的基本原则;
三、新生代的性能调优;
四、老年代的性能调优;
一、性能调优的一些概念和目标
相信对JVM有所了解的开发人员,对于调优过程中牵扯的吞吐性、低延迟/高响应应该不会感觉到陌生。既然生产环境中是大规模的分布式Java平台,JVM吃的内存必然不会太少。不知大家是否还曾记得,64位的JVM能够顺利访问大内存,其最主要的原因是因为其采用了64位的指针架构,这同时也是寻址访问大内存的关键要素。而与之相反的32位的JVM的内存却被限定在了2-3GB上限(与操作平台密切相关,Linux平台,Windows则为1.5G上限)。
大规模的分布式Java平台除了JVM吃的内存特别大外(笔者之前的项目单点持有内存为30GB),为了增加每一个节点的可用性,都是采用多JVM集群的部署模式,这样一来一旦发生单点故障的时候,不会导致整个服务不可用,从而也能够降低单点负载,提升整体程序的执行性能,更好的满足一些特定的高并发场景。
话说生产部署在服务器上的JVM大都是主动或者缺省选择server模式在奔跑,并且在Java7版本之后,JVM缺省开启了分层编译(Tiered Compilation)策略,由C1和C2编译器共同来执行本地代码的编译任务,C1编译器会对字节码进行简单和可靠的优化,以达到更快的编译速度,而C2编译器则会启动一些耗时更长的优化,以获取更好的本地代码编译质量。
那么对JVM进行性能调优的真正目的是什么呢?简单来说就是为了满足程序的高吞吐量、低延迟/高响应性等需求。但是笔者不得不提醒大家,调优是一个循序渐进的过程,必然需要经历多次迭代,最终才能换取一个较好的折中方案。笔者在《Java虚拟机精讲》中曾经提及过,垃圾收集器中吞吐量和低延迟这两个目标其实是存在相互竞争的矛盾,因为如果选择以吞吐量优先,那么降低内存的执行频率则是必然的,但这将会导致GC需要更长的短暂停留时间来执行内存回收。相反如果是选择以低延迟优先,那么为了降低每次执行内存回收时的短暂时间,只能够频繁地执行内存回收,但这又引起了新生代内存的缩减和导致序吞吐量的下降。举个例子,在60s的JVM总运行时间里,每次GC的执行频率是20s/次,那么60s内一共会执行3次内存回收,按照每次GC耗时100ms来计算,最终一共会有300ms(即60/20*100)被用于执行内存回收。但是如果我们将选项“-XX:MaxGCPauseMillis”的值调小后,新生代的内存空间也会自动调整,相信大家都知道,内存空间越小就越容易被耗尽,那么GC的执行频率就会更频繁。之前在60s的JVM总运行时间里,最终会有300ms被用于执行内存回收,而如今GC的执行频率却是10s/次,60s内将会执行6次内存回收,按照每次GC耗时80ms来计算,虽然看上去暂停时间更短了,但最终一共会有480ms(即60/10*80)被用于执行内存回收,很明显程序的吞吐量下降了。因此,在JVM调优这个领域,没有任何一种调优方案是适用于所有应用场景的,同时,切勿极端才能够达到JVM性能调优的真正目的和意义。
二、性能调优的基本原则
简而言之,总而言之,对JVM进行性能调优时,有2个基本原则大家需要进行理解。首先是尽可能的让GC发生在新生代中,也就是尽可能的多执行Minor GC,因为我们都知道Full GC的执行频率尽管不会有Minor GC那么频繁,但是对程序响应性的影响是非常大的(笔者之前的项目Full GC诡异般的执行了50s,显然超出了对响应延迟的容忍度)。那么多让Minor GC执行,显然可以减少触发Full GC的频率。
其次,GC所持有的可用内存越大(Java Heap所占有的堆空间越大),GC的执行效率越好。这是因为内存越大,达到回收阈值就越不容易,那么明显可以提升程序的吞吐量和响应性。当然这并不是说越大越好,如果一个项目JVM撑死只需要1-2G的运行内存,人傻钱多分配120G的内存量,或许程序在稳定情况下运行到硬件故障也不会发生一次Full GC。
既然内存并不是越大越好,总有一个阈值。这就牵扯到生产环境中,开发人员究竟应该如何对Heap分配初始大小?其实这很简单,一个经历过严谨测试的项目,必然会在测试环境中测试N个周期才会移交至生产环境中进行部署,那么在测试环境中,我们可以根据多次迭代后观察Full GC的数据信息来估算生产环境中究竟应该给我们的项目初始多大的内存空间。比如经过多次迭代后,Full GC产生的数据信息中,如果老年代中的活跃数据占用内存大小为100m,那么按照通用的计算法则,可以按照约3-4倍的占用倍数来恒定生产环境中应该分配的堆大小(即-Xms和-Xmx),新生代和老年代的比例官方建议按照整个堆的3/8来进行分配,也就是说选项-Xmn可以占用整个堆内存空间的3/8,这是一种非常简单和通用的计算和分配方式。而永久代则可以按照Full GC后产生的数据信息,根据永久代活跃数据占用内存大小的1.5倍进行恒定生产环境中应该分配的初始值。
这里笔者稍微补充一下,在一些高并发场景下,尤其关注吞吐量和高响应的应用中,应该将-Xms和-Xmx设定为同一值,以此避免内存动态调整时产生的Full GC操作,永久代-XX:PermSize和-XX:MaxPermSize同理。
三、新生代的性能调优
在HotSpot中,串行回收GC与并行回收GC是2个极端,在如今,更多人更倾向于选择后者,并且在一些极其注重吞吐量和高响应的应用场景下,并行回收有着串行回收无法比拟的绝对优势。由于堆空间中的对象大部分都是一些瞬时对象,因此这类对象的生命周期往往更多是由新生代进行“控制”,之前也说过,尽可能的让垃圾收集动作发生在新生代中,而不是Full GC。这样一来,对于新生代的性能调优就主要集中在几个问题上,首先是测量出Minor GC的执行平率和持续时间是否满足需求,以及-XX:ParallelGCThreads选项的配置。如图所示:
如果说Minor GC执行的太频繁,那么必然是-Xmn分配得过小,反之Minor GC很久才执行一次,而每次执行的周期较长,则意味着-Xmn分配得过大。那么究竟应该如何对新生代进行调优呢?简单来说,我们需要多次迭代,从最初将-Xmn的值设置到最低,然后逐步微调,慢慢的你会发现Minor GC的执行频率在降低,直到最终满足需求即可停止。经过这样的调试,你会发现程序的吞吐量上来了,但是每次执行Minor GC的周期会变得较长,怎么办呢?我们可以通过-XX:ParallelGCThreads选项调整GC执行的线程数,让更多的GC线程执行垃圾收集,提升GC的回收效率。这样一来,基本可以满足降低GC的回收平率,提升GC的回收效率。
由于使用的是并行GC,我们可以充分利用多核CPU资源以及线程资源。同微调-Xmn选项一样,我们首先可以将-XX:ParallelGCThreads设置为物理CPU核心数的1/2,比如你的CPU是6核,那么-XX:ParallelGCThreads的值就可以设置为3(最好不要小于2,否则将会影响并行GC的回收效率),这样一来,CPU可用资源就会将一半分配给GC线程使用,而剩下的CPU资源则服务于应用线程中。当然如果你的项目并不重视高响应,-XX:ParallelGCThreads的值可以相对的进行减少,以便于有更多的CPU资源分配给程序中的工作线程。
四、老年代的性能调优
新生代的调优如果大家都已经掌握,接下来我们再来看老年代如何进行性能调优。尽管调优原则中笔者提及过,应该让垃圾收集动作尽可能的发生在新生代中,也就是尽可能多执行Minor GC,但是这并不代表程序永远不会执行Full GC,一旦程序触发Full GC时,所花费的时间往往要大于Minor GC的执行周期,如果Full GC执行的周期过长,对用户所带来的直观感受是非常不友好的,比如用户在执行登录操作,恰恰悲催的碰见JVM正在执行长时间的Full GC,请自行补白。。。
在GC的命令选项中并不存在直接设置来年代内存大小的选项,那么老年代的内存大小如何设置呢?简单来说,老年代的内存空间大小间接等于-Xmx的值减去-Xmn的值,比如-Xmx为120G,-Xmn的值为45G,那么剩下的75G就是老年代的内存空间。在此大家需要注意,如果当-Xmn产生变化时,-Xmx也要随之成比例的发生变化,否则老年代占用的内存空间将会增大或变小,如果增大,Full GC的执行周期将会变得更长,反之执行频率将会频繁。
一般来说,如果<=3G以下的堆内存,建议使用的GC组合是Parallel和Parallel Old,除非真的是需求无法容忍系统出现长时间的“Stop the World”(目前几乎没有任何一款GC不需要暂停工作线程,只是尽可能的缩短暂停时间,包括G1)情况下,才推荐上CMS,不过一般大内存的使用,老年代首推CMS执行垃圾收集,并且CMS也是除G1之外的HotSpot中唯一的一款可以单独执行老年代增量回收,而不必执行Full GC全量回收的垃圾收集器(Promotion Failed和Concurrent Mode Failed情况除外)。
之所以要用CMS,是因为CMS天生为低延迟/高响应而生。因为CMS的执行过程中,只有初始标记和再次标记会出现暂停,而其它过程CMS的工作线程将会和程序的工作线程同时工作,大大提升了GC的回收效率。那么使用CMS同样需要进行优化,其中最主要的就是调整-Xmx的大小和-XX:CMSInitiatingOccupancyFraction选项。如图所示:
-XX:CMSInitiatingOccupancyFraction用于设置老年代中的内存使用率达到多少百分比的时候执行内存回收(低版本的JDK缺省值为68%,JDK6及以上版本缺省值则为92%),在JDK6以后续版本中,如果按照缺省配置,当老年代的内存使用率达到92%后才进行垃圾收集,这往往会导致从新生代晋升到老年代中的对象将无法进行存放。
如果-XX:CMSInitiatingOccupancyFraction设置得太低又会导致CMS GC触发的频率太快。一般来说,在大内存的堆使用上,笔者将这个值设置在70-80之间算是比较合理的。
如果你对Java高并发、分布式、JVM、微服务等技术感兴趣的可以进我的Java交流群:582100479,欢迎大家转发讨论!
尽管CMS是大内存的首选,但是CMS仍然是有一些令人不满意的地方,比如抢占CPU资源、内存碎片等问题。不过总而言之,CMS目前在大内存的使用上,仍然是首选。