Java GC 及HBase RegionServer GC调优

原文地址:https://blog.csdn.net/wwwxxdddx/article/details/50981089

1背景

1.1问题描述

HBase RegionServer由于GC等原因Stop World超过40s,RS在ZK上创建的临时节点被删除,造成Master认为RS已经下线,重新分配该RS上的Region。RS恢复后,由于种种原因(WAL被其它RSSplit并删除,Master通知RS下线等)只能主动退出。

经过长时间观察和参考网络资料,确定和HBase RS GC有关,需要调整GC参数以降低Full GC的频率。

1.2JVM GC简介

Java GC(Garbage Collection,垃圾收集,垃圾回收)机制主要负责3件事:1、确定哪些内存需要回收,2、确定什么时候需要执行GC,3、如何执行GC。该机制对 JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收。然后,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,防止出现内存泄露和溢出问题。

如JVM架构图,JAVA程序运行时的内存可以分为5个部分,这些模块的各自作用如下:程序计数器,每个线程有一个独立的程序计数器,记录下一条要运行的指令。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空;虚拟机栈 ,线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的私有变量、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果栈的深度是固定的,请求的栈深度大于最大可用深度,则抛出StackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutOfMemoryError;本地方法区,和虚拟机栈功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用C实现的;JAVA堆,线程共享的,存放所有对象实例和数组。垃圾回收的主要区域;方法区,线程共享的,用于存放被虚拟机加载的类的元数据信息:如常量、静态变量、即时编译器编译后的代码。也称为永久代。如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收。

常见的GC策略有:标记-清除算法(Mark-Sweep),从根节点开始标记所有可达对象,其余没标记的即为垃圾对象,执行清除。但回收后的空间是不连续的;复制算法(copying),将内存分成两块,每次只使用其中一块,垃圾回收时,将标记的对象拷贝到另外一块中,然后完全清除原来使用的那块内存。复制后的空间是连续的。复制算法适用于新生代,因为垃圾对象多于存活对象,复制算法更高效;标记-压缩算法(Mark-compact),适合用于老年代的算法(存活对象多于垃圾对象)。标记后不复制,而是将存活对象压缩到内存的一端,然后清理边界外的所有对象。

 目前java中可作为GC Root的对象有:虚拟机栈中引用的对象(本地变量表);方法区中静态属性引用的对象;方法区中常量引用的对象;本地方法栈中引用的对象(Native对象)。

1.3分代GC

据IBM统计,98%对象瞬间消失,仅有2%对象存活较长时间。 HotSpot VM将堆分为Young generation和Old generation。大部分对象在Young generation创建,很快就不可达,发生在该区域的GC称为 "minor GC" 。Young generation 中存活下来的对象被拷贝(Promotion)到Old generation,该区GC频率较低,发生在该区的GC称为majorGC" (或"full GC") 。 

绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快。Minor GC过程如下: 当Eden区满的时候,执行MinorGC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的); 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值将对象提升到老年代)之后,仍然存活的对象,将被复制到老年代。

 年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC(或Full GC)。可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。如果对象比较大(比如长字符串或大数组),Young空间不足,则较大的对象会直接分配到老年代上。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:类的所有实例都已经被回收;加载类的ClassLoader已经被回收;类对象的Class对象没有被引用(即没有通过反射引用该类的地方)。

1.4常见GC策略

Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)

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

ParallelScavenge 收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)

 Serial Old收集器:老年代收集器,单线程收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。

Parallel Old收集器:老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。

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

G1收集器:JDK7开始支持,面向服务器的垃圾收集器,旨在取代CMS。

1.5CMS GC异常

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

promotion failed:是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;

concurrent modefailure:是在执行CMS GC的过程中同时有对象要放入旧生代,而此时老年代空间不足造成的。

造成这些异常的原因主要有:老年代内存不足,需要提前进行CMS;老年代内存碎片化,需要进行压缩整理。

2GC调优实战

2.1常见JVM参数

JVM参数分为:标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用(但是,这些参数往往是非常有用的)。

非标准参数示例:

-Xms 为Heap区域的初始值,线上环境建议与-Xmx设置为一致

-Xmx 为Heap区域的最大值

-Xmn1024k,-Xmn512m,-Xmn1g(-Xms,-Xmx也是种写法)

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

    1. -XX:+<option> 启用选项

    2. -XX:-<option> 不启用选项

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

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

2.2看懂GC日志

JVM的GC日志的主要参数包括如下几个:

-XX:+PrintGC 输出GC日志

-XX:+PrintGCDetails 输出GC的详细日志

-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)

-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式, 2013-05-04T21:53:59.234+0800)

-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息

-Xloggc:../logs/gc.log 日志文件的输出路径

ParNew GC日志

[GC2016-03-25T02:35:26.074+0800:35289.315: [ParNew: 885174K->48101K(943744K), 0.0197790 secs]10343272K->9506199K(16672384K), 0.0199760 secs] [Times: user=0.27 sys=0.01,real=0.02 secs]

[GC时间: JVM启动时间: [类型: GC前年轻代大小A-> GC后年轻代大小B(年轻代总大小C), 耗时] GC前堆大小D->GC后堆大小E(堆的总大小F),GC总耗时] [Times: user=0.27 sys=0.01, real=0.02 secs]

B是GC后存活的对象大小,这里是不是需要分析一下MaxTenuringThreshold;F-C老年代堆大小;E-D此次GC释放内存大小;(E-D)-(B-A)年轻态提升到老年代的大小;(D-A)/(F-C)GC前老年代使用比例;(E-B)/(F-C)GC后老年代使用比。

CMS GC日志

2016-01-15T12:58:23.597+0800: 4215.939:[GC [1 CMS-initial-mark: 11025382K(15728640K)] 11130602K(16672384K), 0.0848720secs] [Times: user=0.0

3 sys=0.05, real=0.09 secs] 【1.初始标记,暂停JVM】

2016-01-15T12:58:23.682+0800: 4216.024:[CMS-concurrent-mark-start]

2016-01-15T12:58:24.052+0800: 4216.393:[CMS-concurrent-mark: 0.369/0.369 secs] [Times: user=2.05 sys=0.04, real=0.37secs] 【2.并发标记】

2016-01-15T12:58:24.052+0800: 4216.393:[CMS-concurrent-preclean-start]

2016-01-15T12:58:24.091+0800: 4216.433:[CMS-concurrent-preclean: 0.039/0.039 secs] [Times: user=0.04 sys=0.00,real=0.04 secs] 【3.并发预清理】

2016-01-15T12:58:24.091+0800: 4216.433:[CMS-concurrent-abortable-preclean-start]

 CMS: abort preclean due to time2016-01-15T12:58:29.188+0800: 4221.530: [CMS-concurrent-abortable-preclean:5.094/5.097 secs] [Times: user=5.77 sys=0.18, real=5.09 secs]

2016-01-15T12:58:29.189+0800: 4221.531:[GC[YG occupancy: 317194 K (943744 K)]2016-01-15T12:58:29.189+0800: 4221.531:[Rescan (parallel) , 0.0365570 secs]2016-01-15T12:58:29.226+0800: 4221.567:[weak refs processing, 0.0047560 secs]2016-01-15T12:58:29.231+0800: 4221.572:[scrub string table, 0.0006960 secs] [1 CMS-remark: 11025382K(15728640K)]11342577K(16672384K), 0.0425260 secs] [Times: user=0.57 sys=0.01, real=0.05secs] 【4.重新标记,暂停JVM】

2016-01-15T12:58:29.232+0800: 4221.574:[CMS-concurrent-sweep-start]

2016-01-15T12:58:30.903+0800: 4223.245:[CMS-concurrent-sweep: 1.666/1.671 secs] [Times: user=1.98 sys=0.06, real=1.67secs] 【5.并发清除】

2016-01-15T12:58:30.903+0800: 4223.245:[CMS-concurrent-reset-start]

2016-01-15T12:58:30.959+0800: 4223.300:[CMS-concurrent-reset: 0.055/0.055 secs] [Times: user=0.03 sys=0.02, real=0.05secs] 【6.并发重置】

如果不发生异常,我们只需要关注初始标记和重新标记。

 promotionfailed日志格式

2016-01-29T00:09:17.127+0800: 1167669.469:[GC2016-01-29T00:09:17.127+0800: 1167669.469: [ParNew (promotion failed): 943655K->943655K(943744K

), 0.1652200 secs]11986693K->12025719K(16672384K), 0.1655820 secs] [Times: user=0.99 sys=0.01,real=0.16 secs]

concurrent modefailure日志格式

2016-01-18T08:49:18.984+0800: 248471.326:[GC2016-01-18T08:49:18.984+0800: 248471.326: [ParNew (promotion failed): 943734K->924083K(943744K), 0.1256890secs]2016-01-18T08:49:19.110+0800: 248471.452:[CMS2016-01-18T08:49:23.288+0800: 248475.629: [CMS-concurrent-sweep:4.197/4.347 secs] [Times: user=4.79 sys=0.02, real=4.35 secs]

 (concurrent mode failure):11272744K->8215769K(15728640K), 10.5375280 secs]12182463K->8215769K(16672384K), [CMS Perm : 45065K->45060K(131072K)],10.6635700 secs] [Times: user=10.95 sys=0.04, real=10.67 secs]

 Full GC日志格式

2016-01-29T00:09:17.293+0800: 1167669.635:[Full GC 2016-01-29T00:09:17.293+0800: 1167669.635:[CMS2016-01-29T00:09:17.456+0800: 1167669.798:

[CMS-concurrent-abortable-preclean:0.447/0.637 secs] [Times: user=2.30 sys=0.19, real=0.64 secs]

 (concurrent mode failure):11082063K->7407560K(15728640K), 5.8053900 secs]12025719K->7407560K(16672384K), [CMS Perm : 45764K->45492K(131072K)],5.8056040 secs] [Times: user=5.75 sys=0.01, real=5.81 secs]

这个不是promotion failed引起的,应该是有较大的对象直接在老年代分配失败造成的,很少出现该现象。

2.3HBase调整CMS参数

原有参数

HBASE_HEAPSIZE:16G

-XX:+UseConcMarkSweepGC:设置年老代为并发收集

-Xmx16G -Xms16G:-Xms、-Xmx 相等以避免在GC 后调整堆的大小。

-Xmn1G:指定New Generation的大小, Yong区设置过大GC时间长。

-XX:PermSize=128M -XX:MaxPermSize=256M

-XX:SurvivorRatio=8:设置堆内存年轻代中Eden区与Survivor区大小的比值 。设置为8,则两个Survivor区与Eden区的比值为2:8,每个Survivor区占 整个年轻代的1/10。 

第一次新增

-XX:CMSInitiatingOccupancyFraction=70:表示年老代空间到70%时就开始执行CMS,确保年老代有足够的空间接纳来自年轻代的对象。

-XX:+UseCMSInitiatingOccupancyOnly:使用CMSInitiatingOccupancyFraction的值作为old区的空间使用率限制来启动CMS垃圾回收。

-XX:+UseGCLogFileRotation-XX:NumberOfGCLogFiles=3 -XX:GCLogFileSize=128M:GC日志rotate

-XX:+UseParNewGC:设置年轻代为并发收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

-XX:+CMSParallelRemarkEnabled:并发标记,降低标记停顿,1.6以上默认true。 

依据: CMS GC发生时剩余空间到达90%左右,极易发生promotion failed和concurrent mode failure,调整CMSInitiatingOccupancyFraction使CMS提前发生,给从年轻代提升的对象预留足够的空间。

效果:16台机器RS宕机频率由原来2~3天出现一台,下降至一个月出现一次

如上图,老年代使用达到了90%,此时发生promotion failed,需要提前进行CMS GC。

第二次新增

-XX:+UseCMSCompactAtFullCollection设置在FULL GC的时候,对年老代的压缩;CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片 。

-XX:CMSFullGCsBeforeCompaction=0这个参数,指定进行多少次full GC之后,进行内存空间压缩,0是每次都会。

依据:老年代使用70%时CMS正常发生时出现concurrent mode failure,或老年代空间足够时出现promotionfailed,判断是老年代碎片化严重导致,需要对老年代压缩整理。

效果:2台机器RS宕机频率由原来一个月可能出现一台,修改至今一个多月没有出现,也没再出现promotion failed和concurrent mode failure等异常

如上图,CMS时出现concurrentmode failure,但是空余内存空间3GB以上,属于内存碎片化造成的异常。

猜你喜欢

转载自blog.csdn.net/qq_36421826/article/details/82016899