首页 > 代码库 > JVM那些事儿(二)——垃圾回收

JVM那些事儿(二)——垃圾回收

这节小汪介绍一下jvm的垃圾回收机制,首先我们先提问:

1.为什么要有不同的垃圾算法

2.垃圾回收器要解决的终极目的是什么

3.小汪该如何选择自己的垃圾回收器


一、垃圾回收算法

众所周知,java堆内存的垃圾回收由java虚拟机管理,目前java有几种算法用来解决垃圾回收(以下只介绍最重要的两个算法)

1.1 复制算法

技术分享

如图所示,复制算法可以说是最直观最简洁的算法了。按照复制算法的思路,内存要分为两块 Eden Survivor区域,Eden有一个,Survivor有两个。

首先,各种对象都在Eden+一个Survivor里

其次,当Eden+一个Survivor满的时候,收集开始时,Eden+一个Survivor的存活对象会copy到另一个Survivor中

最后,清除Eden +一个Survivor

以上就是一次复制算法的过程

复制算法小结

1.Survivor是用来保存存活对象,为了保证对象能被合理清除和复制,永远会有一个Survivor是空闲的,以用来进行下一次回收。

2.jvm对Eden和Survivor的内存比例默认是8:1

3.copy和清除时 其余线程要等待(仔细想一想,如果其余线程不等待,那复制的过程会严重伤害其余线程所使用的对象,导致线程bug)

优劣势

1.内存空间闲置:复制算法天生只适合于年轻态,因为年轻态对象朝生夕灭,所以copy的对象并不是很多,Survivor可以分配一个很小的空间(8:1)。但是还是会存在内存闲置(10%的空间),尤其是当每次回收都会存活很多对象的时候,不适合于老年代。

2.copy的瞬间会出现卡顿情况(即所有线程等待),但是其卡顿时间要小于其他算法。(后面会解释为什么)


1.2 标记-整理算法

技术分享


为解决复制算法会带来产生内存空闲,产生了标记-整理算法。

如图所示 该算法分为以下几步:

1.标记要回收的对象(会产生卡顿时间)

2.将存活对象移动到内存的一侧

3.直接删除掉边界以外的内存空间

其思想是基于标记-清除演化而来。

优劣点

1.不用多余的内存空间

2.回收效率慢,由于需要标记,同时内存要移动位置其执行时间大于复制算法。所以它不适合新生代

综上所述,目前jvm回收器会根据年代的不同使用不同的算法收集,新生代使用复制算法 老年代使用标记-整理算法

二、垃圾回收器

2.1垃圾回收器的终极目的

jvm有多种垃圾回收器,都是基于上述两种算法实现的,辣么多回收器,其终极目的是解决以下两个问题。

1.卡顿时间

2.吞吐量

卡顿时间:cpu停止作业执行收集任务的时间吞吐量:在一定时间内cpu运行代码的时间与总时间的比值,可以理解为cpu在一定时间内有多少时间是用来干活的,而不是“偷懒的”。

卡顿时间和吞吐量其实存在一定得矛盾,这里小汪必须解释清楚。

如果仅仅追求低的卡顿时间,可能带来吞吐量的下降,这是因为低卡顿 使收集的垃圾总量变少了。

举例:如果卡顿1秒可以收集100m 那么当卡顿500ms 可能仅仅能收集40m(因为每次卡顿还要有其他时间的支出) ,这样如果要收集1000m的内存,每次的卡顿时间虽然很少,但是卡顿次数增加了,卡顿时间总和其实是上升的,最终会导致吞吐量的下降。

为了解决上面两个终极问题,sun推出了一系列各有千秋的jvm收集器,接下来小汪会一一介绍

2.2 垃圾回收器列表

技术分享

如图所示 jvm主要有以下几种收集器

年轻态:

Serial 单线程回收器

ParNew 多线程回收器

Parallel 吞吐量优先回收器(多线程)

Parallel Old 吞吐量优先回收器老年版

Serial Old 单线程回收器老年版

CMS “牛逼的” 高并发低停顿回收器

G1 这才是最牛逼 最高大尚的回收器


介绍各种收集器之前,先说一下个人使用的经验吧。

一般来说,在设置jvm参数时,会有以下两种设置

1.Parallel + CMS  追求吞吐量

2.ParNew + CMS 追求低卡顿

多线程的问题——并发和并行

并行:用户线程停止 多线程并行回收垃圾

并发:用户线程和垃圾回收线程一起办公

多线程的问题——并发会引起低效率

用到多线程 还需要设置回收线程的个数 这个要根据cpu的核数来定,一般的,一个线程会占用一个核,cpu很少的情况下,会导致并发回收时,回收器会抢占部分cpu,而真正跑作业的cpu变少,影响效率。

这里利用CMS收集器举例来说:

CMS收集器默认的回收线程数 = (cpu数量 + 3) /  4 大概占用25%的线程

但是当线程数量很少,比如为2 那么有一个线程专门用来回收,这时大作业的任务都挤在一个线程上,会非常影响效率。

所以小汪认为,如果服务器的线程数量很少,且你的项目又是大数据量体验极强的项目,那不妨设置成单线程回收器,总体来说可以比多线程回收器要好。


好啦,下面小汪开始逐一介绍回收器


2.3 Serial回收器

先上图

技术分享

单线程收集,顾名思义,即在垃圾回收时,独占进程进行回收,作业需要停止

其新生代采用复制算法

老年代采用标记-整理算法


优劣点:

1.业务卡顿时间长,当遇到大规模程序需要频繁gc的时候,严重影响业务的体验。

2.好的方面,利用全部资源专注于垃圾回收,其实收集几百mb的内存 也就需要几十ms 相比多线程,可以缩短垃圾回收,当然前提是垃圾回收不频繁。。。。

小结:简单高效

2.4 ParNew回收器

技术分享
开启多线程并行进行垃圾回收,在多核cpu中,其执行效率要远远高于Serial,当然也会造成卡顿。

优势:
1.ParNew + CMS  目前CMS仅仅支持和ParNew的配合工作(此外也支持Serial)

2.默认开启与cpu数量相同的回收线程

小结:多核无敌

2.5 Parallel回收器

相比 ParNew 其关注的是系统吞吐量 , 可通过参数设置最大停顿时间和吞吐量大小
目前Parallel + Parallel Old 搭配使用
技术分享

优劣点:
低卡顿 适用于用户体验极高的场景,在交互场景中 我们就需要保证每次的卡顿时间最短
吞吐量优先 适用于后台大规模作业 其可以有效利用cpu 尽快完成作业任务
小结:
吞吐第一

2.6 CMS回收器

CMS是目前使用最广的收集器
技术分享
CMS收集器致力于降低卡顿时间 在多核处理下 相比上述回收器 其卡顿时间应该是最少的
基于标记-清除算法
其回收步骤分为4部
 1.初始标记:卡顿;标记与GCRoot直接关联的对象
 2.并发标记:与用户线程并发的条件下 进行根搜索标记
 3.重新标记:卡顿;修正并发标记期间因程序运行而导致标记产生变动的那部分记录
 4.并发清除:并发条件下进行清除工作

为什么有的卡顿有的没卡顿呢??
卡顿是为了防止同一时刻用户线程产生的新的垃圾
CMS回收器很清楚的认识到,首先进行卡顿 确定一个需要标记的范围。所以初始阶段不能并发,因为并发会产生新的对象,会让初始标记一直标记下去。
其次 根搜索标记可以并发进行,这是对前面卡顿的初始标记的进一步标记这样,这时候并发所产生的新的对象都留给后面处理
再次 重新标记是为了处理这段期间内的对象 所以必须卡顿
最后,标记结束进行并发清理。
那么问题来了,并发清理期间的垃圾怎么解决 并发期间用户线程需要的内存空间怎么解决?
这两部分归根结底还是内存空间的问题,这需要CMS在这次GC并发收集时留有足够的空间给用户线程
      如果在并发收集时内存不够了,会出现“Concurrent Mode Failure” ,这是会逼迫虚拟机改用Serial Old ,即卡顿一切 进行垃圾收集。

优劣点:
1.适合多CPU情况(在上面已经讨论了CPU极少情况下的后果)
2.会产生“Concurrent Mode Failure” 需要程序猿凭经验进行处理
3.采用标记-清除算法 会产生多余碎片,这是可以设置参数时虚拟机定期对内存进行整理,当然 这会提供卡顿时间影响效率。

小结:
干活我最快

2.7 G1回收器

技术分享
java1.7中加入的新回收器 目前还处于试验阶段 
实现了终极目的:在不牺牲吞吐量的情况下完成低卡顿的垃圾收集
设计思路:避免全区域的内存回收
将堆重新布局 新老代不再是物理隔离而只是逻辑上的区分 ,将堆分为不同的Region区域,每个Region对应一个Remember Set。

全区域内存回收问题
所有收集器都会遇到,如果一个区域的对象引用了其他区域的对象,在回收时是否需要对所有区域进行扫描,可能会进行全栈(就是全区域)扫描。
               如何避免:虚拟机使用Remembered Set 保存这块区域引用的其他区域对象的一个记录,这个Set位于这块区域内。回收时针对Set进行回收就好了。
回收步骤:
1.初始标记:卡顿;标记与GCRoot直接关联的对象
2.并发标记:与用户线程并发的条件下 进行根搜索标记
3.重新标记:卡顿;修正并发标记期间因程序运行而导致标记产生变动的那部分记录
4.筛选回收:对Region按价值的排序,根据用户期望的GC停顿时间来制定回收计划

小汪在实践中还没有用到过G1回收器 ,目前对其的认识也比较肤浅,还请大牛们多多传授G1相关的经验。

小结:我最牛逼 不过我还不够成熟

三、选择回收器和相应配置参数

还是结合实践说吧

目前我在做一个秒杀项目,cpu为32核 流量每天大概有3000万次,代码里除了很少的静态对象,其余大多是朝生夕灭的对象。
所以我的项目应该归纳为:高体验,新生代频繁收集(大概每天好几千次),老年代低频使用

3.1 追求高体验 低卡顿型

我的配置参数:
设置老年代为cms收集器
设置年轻代为并行收集器(jdk1.5以上 系统会自动设置年轻代收集器)

-Xms1024m -Xmx1024m -Xmn512m -Xss128k -XX:NewRatio=4 -XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC:
-XX:CMSInitiatingOccupancyFraction=80
-XX:+PrintGCDetails
-XX:CMSFullGCsBeforeCompaction=5: 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片

运行一段时间后,使用jstat -gc 总结 一次新生代时间大概在10ms左右 每天会发生3000次ygc 10次左右fgc 一次fgc时间大概在40ms,秒杀总体时间在300ms左右,这种回收配置参数还是可以接受的。

3.2 追求高吞吐量

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

-XX:+UseParallelGC

-XX:ParallelGCThreads=20
-XX:MaxGCPauseMillis=100 
-XX:+UseAdaptiveSizePolicy :设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开


参考文献
1.《深入理解java虚拟机
2. javaG1垃圾收集器http://blog.csdn.net/woshiqjs/article/details/7290513
3. jvm垃圾回收机制总结  http://hxraid.iteye.com/blog/746064

展望:
后期小汪还会出一些C php python相关垃圾回收机制的文章 以及对jvm垃圾回收器源码的源码理解



JVM那些事儿(二)——垃圾回收