GC策略笔记备忘(被namenode所迫)

转自:http://blog.csdn.net/ae86_fc/article/details/6244164

引 起namenode占据这么多内存的原因就不详细记了,经历过的人肯定都知道。既然这哥们占了这么大内存,那么其对应的GC肯定就表现的与众不同。在这个 上面,我们真的吃过很多苦头,以前人傻,比较天真,也没有什么经验(估计SUN开发JVM的时候也没有想到过有人会把他们的产品用到这种程度),不知道在 某种情况下,会出现某种现象,导致namenode表现变得很奇怪,甚至一段时间失去响应(好几秒钟),造成的后果也很严重。所以不得已,只好去对 java的GC策略,GC算法以及GC的tuning进行深入的学习,调整GC策略,来满足在高负荷负载,大内存namenode使用的情况下,尽量让 HDFS平稳服务的办法。(这些都是被逼的。最近看一本书,上面说了一句话,大致意思是说:让人进步,最大的老师是兴趣。但还有更狠的说:让人进步,最大 的老师是生存。)这些经验一直很零散,也没有经过大师们的review,但还是整理一下吧。有错误的请各位大师批评指导。 

JVM内存 arrangement 

每个JVM默认会将其管理的内存结构分成类似如下图所示的结构: 
《GC策略笔记备忘(被namenode所迫)》 
其 中,将其heap分成三个大区,young区,tenured区,和permanent区。区的内部还会有更细的划分。整体说来,就是young区保存的 是一些生命周期最短,存在时间最长,最近才刚刚分配的一些对象,对young区的内存collection称之为minor collection。而tenured区中是生命周期更长,需要在程序运行中相对比较长期保存的对象内存区,对tenured区的collection 称之为major collection。而perm区就是用来保存整个java虚拟机运行中需要用到的对象,这个区通常不怎么太多的关注。 
对于GC策略的选择和评估,主要的评估指标通常包含以下几种:

  • throughput:用在非GC上的时间,包括内存分配和释放的时间
  • Pauses:jvm在做GC的时候应用程序停止响应的时间
  • Footprint:进程的workset。在内存很有限,但进程非常多的机器上,通常就可以用它来表示scalability。
  • Promptness:回收灵敏度,对象被认为died到其内存被回收的时间间隔


通 常来说,对每个区的size规划和jvm表现出来的性能方面都是一些trade-off。比如说,如果young区设置的比较大,那么对young区进行 collection的次数就会比较少,那么throughput就会响应的高,同时pause time,promptness就会响应的降低。而将young区设置的小一点,collection的次数就会频繁一些,但同时pause就会变小,回 收灵敏度也会提高。具体怎么样划分,取决于应用进程的需要,看它是对响应时间敏感,还是对过时对象内存被回收的时间敏感,或者其他等等。 

选择不同的GC策略,就会有不同的表现,要记录这些表现,可以通过打印GC日志来获取。使用 -verbose:gc 命令行参数就可以使jvm在每次进行内存回收的时候打印出回收日志信息。如下: 
[GC 325407K->83000K(776768K), 0.2300771 secs] 
[GC 325816K->83372K(776768K), 0.2454258 secs] 
[Full GC 267628K->83769K(776768K), 1.8479984 secs] 
这里可以看出,jvm在运行过程中进行了两次minor collection和一次major collection,箭头两边的数字 
325407K->83000K ( 第一行) 
表示:在这次minor collection的前后,live objects占用的内存大小分别为多少。后面的日志 
(776768K)( 第一行) 
表示该jvm还剩下的内存空间有多少,不包括perm区。最后的日志 
0.2300771 secs 
表示这次minor collection耗费了多长时间(230毫秒) 

第三行的major collection的日志也差不多。不过,当使用了jvm参数 -XX:+PrintGCDetails 后,jvm在每次collection的日志中会包含更详细的信息。如以下格式: 
[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]] 
该日志表示:这次minor collection回收了98%的young内存区的die对象( 64575K->959K(64576K) ),并且耗费了46毫秒的时间。整个heap使用的内存下降了51%( 196016K->133633K(261184K) )。同时还做了一些额外的开销,总共耗时45.9毫秒。 
-XX:+PrintGCTimeStamps 参数还会将每次collection的日志前加上时间戳。如下: 

111.042: [GC 111.042: [DefNew: 8128K->8128K(8128K), 0.0000505 secs]111.042: [Tenured: 18154K->2311K(24576K), 0.1290354 secs] 26282K->2311K(32704K), 0.1293306 secs] 
表示这次collection发生在该java进程运行后的111秒,minor collection几乎在同时发生,另外,还记录了tenured区的collection记录,可以看出,tenured区在这次回收中回收了大约10%的空间 
18154K->2311K(24576K) 
并耗费了129毫秒。 

了解了GC日志后,就需要对jvm中内存的几个区域划分进行规划了。这中间就包括几个关键的设置: 
-Xmx : 该jvm最多使用多少内存 
-Xms : 该jvm最少使用多少内存 
由于collection会在区域被填满使进行,所以区域中可用内存的大小是跟程序的throughput成反比的。而整个jvm能够使用的内存就是一个非常重要的性能参数。 

默认情况下,jvm会在每次collection后扩充或者收缩其heap内存,来满足剩余heap空间和live对象数量维持一定的比例。该比例是由另几个jvm参数来制定: 
-XX:MinHeapFreeRatio=<minimum> 40 
-XX:MaxHeapFreeRatio=<maximum> 70 
分 别表示最小和最大的heap free比例,一次来防止jvm的heap size过度频繁的伸缩,提高性能。默认情况下,其默认值分别为40%和70%。也就是说,整个heap里最多有70%的内存空间是free的,超过这个 值,jvm就会收缩heap(在-Xms限定的范围内),而free内存效果40%,jvm就会扩充heap size(在-Xmx限定的范围内)。而在64位的机器上,该比例会被响应的放大30%左右,来抵消64位机器上对象内存更多的消耗。 

young区tuning 
除 了整个heap的大小比较关键外,设置young区的大小也非常重要,young区越大,那么minor collection发生的频率就越低,但是,响应的tenured区的大小就会越小,进而导致major collection变得频繁。所以,该比例的制定,取决于应用程序的需求。 

默认情况下,young区的大小是被参数 
-XX:NewRatio=3 
来设置,该设置标识,young区和tenured区的大小比例为:1:3,也就是说,young区大约占整个heap size的1/4左右。对young区的划分并不一定非要用比例,也可以像类似-Xmx和-Xms的方式来限制young区的大小。如下: 
-XX:MaxNewSize=size 
-XX:NewSize=2.125m 
用这两个参数,就能限制整个jvm中young区大小的上限和下限。 

GC算法和策略 
java中有不止一种的GC模式,响应的,对应每一种GC策略,都有响应的GC collector。目的都是为了在不同的应用模式下,尽量提高应用程序的throughput和减少GC带来的pause时间。 
目前j2se中有以下几种非默认的GC collector:

  1. The throughput collector


该collector使用并行的young区collector进行回收。要使用该GC策略,可以使用参数 
-XX:+UseParallelGC 来指定。而tenured区的回收策略仍然跟默认的回收器一样。 

  1. The concurrent low pause collector


要使用该回收器,可以使用参数-XX:+UseConcMarkSweepGC 来指定。该回收器是用来回收tenured区的并且回收工作会跟应用程序并行执行。应用程序会在回收过程中有短暂的停顿。 

  1. The incremental (sometimes called train ) low pause collector:


该回收器不推荐使用,也不会更新,就不看了。 

所 以,由于namenode内存大部分的对象都是文件,目录和blocks,这些数据只要不被程序或者数据的拥有者人为的删除,就会在namenode的运 行生命期内一直存在,所以这些对象通常是存在在tenured区中,所以,如果整个hdfs文件和目录数巨多,blocks数也巨多,撑了几十G的内存, 那么这种情况下,就比较适合使用c oncurrent low pause collector的GC策略,让对tenured区的回收跟namenode进程同时并行,减少tenured区回收时namenode服务进程的延迟。 
选 择这种GC策略,通常都是希望降低对tenured区进行回收时应用程序的pause时间。这种策略使用一个独立的gc 线程来进行资源回收。对于每一次major collection,回收器都会在回收开始的时候将应用程序线程hang住,直到回收进行到一定时期。而第二次通常会比较长,并且在此期间会有多个线程 对内存进行回收。 

该 回收器会在每次回收时将应用程序pause两次,第一次会将所有从root对象出发可达的对象标记为live。这次pause被称为initial mark。而第二次pause会在mark阶段和寻找在并行mark期间由于应用程序的修改而没有找到的live对象。整个过程为remark。 

当 使用默认的垃圾蒐集器时,major collection会在tenured区被填满的时候发生,而此时应用程序线程就会被完全hang住,直到major collection完成。相比只下,concurrent collection需要在一个时间点开始,使得从这个时间点开始collection的话,那么collection的结束时间会早于tenured区 被填满的时间。以此来避免应用程序被hang住。所以,concurrent collector会在内部保存一些状态信息,其中就包括从现在开始进行major collection的结束时间(T-collect)以及从现在开始tenured区被填满的时间(T-until-full)。当T-until- full超过T-collect时,那么collection就要开始执行。这样就能保证collection在区域被填满之前完成。 concurrent collection同样也会在tenured区被占用到一定程度后开始执行。默认情况下,该比例为68%左右。但该比例可以通过jvm参数进行设置: 
-XX:CMSInitiatingOccupancyFraction=<nn> 
<nn>表示占用tenured区内存大小的比例。通过该选项,就可以控制concurrent collection的时机。

点赞