JVM系列之(2)——垃圾回收算法和垃圾收集器

1、垃圾回收方法

标记清除复制——浪费一部分内存,但是不需要移动对象。适合新生代,少量对象存活。


标记清除整理——适合老年代(大部分对象存活,需要移动的对象不多),并不会每次GC都做整理,具体垃圾收集器做设置。


2、垃圾收集器


不同垃圾收集器的区分:

新生代、老年代垃圾收集器,

单线程、多线程并行垃圾收集器,

stop the world和并发(GC线程和用户线程同时运行)的垃圾收集器

如果你运行在JVM的客户端模式(Client)下,JVM默认垃圾收集器是串行垃圾收集器(Serial GC,-XX:+USeSerialGC);在JVM服务器模式(Server)下默认垃圾收集器是并行垃圾收集器(Parallel GC,-XX:+UseParallelGC)。

(1)、serial、serial old收集器:

单线程,不存在线程之间切换,适用于client模式。

Serial old会作为CMS收集器concurrent mode failure失败时的替代收集器。

(2)、ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本(也是一个新生代收集器),除了使用多条线程进行垃圾收集外,其余行为都与Serial收集器完全一样,共用了相当多的代码。

ParNew垃圾收集器作为CMS收集器的默认新生代收集器,也可以通过-XX:UseParNewGC选项来强制指定,使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

(3)、Parallel Scavenge

Parallel Scavenge也是新生代并行垃圾收集器。和ParNew不同的是,Parallel Scavenge收集器的目标是达到可控的吞吐量,吞吐量优先,吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。主要适合在后台运算而不需要太多交互的任务。

-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间,允许的值是一个大于零的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值(但是不是说这个值设置的越小,垃圾收集的速度就越快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间换取的:系统把新生代调小一些,收集300M新生代肯定比收集500M快,这也导致垃圾收集发生的更频繁,原来十秒收集一次、每次停顿一百毫秒,现在变成五秒收集一次、每次停顿七十毫秒,停顿时间的确下降了,但是吞吐量也下降了)。

-XX:GCTimeRatio 直接设置吞吐量大小,参数的值应当是一个大于零小于一百的整数,相当于吞吐量的倒数。如果此参数值为19,那允许垃圾收集的时间就占总时间的5%(即1/(1+19)),默认值是99

-XX:UseAdaptiveSizePolicy 这是一个开关参数。当这个参数打开之后,就不需要手工指定其他参数等细节了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最何时的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。这是一个不错的选择,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,具体细节就不用管了。

Parallel Old收集器

此收集器是Parallel Scavenge收集器的老年代版本,这个收集器出现之后,“吞吐量优先”收集器终于有了名副其实的应用组合。其运行过程和Parallel Scavenge类似。

(4)、CMS收集器

此收集器用户线程和GC线程可以并发进行,是一种以获得最短回收停顿时间为目标的收集器。大量用在B/S系统的服务器上,因为这类应用注重响应速度,给用户较好的体验。

-XX:+UseConcMarkSweepGC:激活CMS收集器,默认情况下使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。

初始标记

CMS算法中两个会触发Stop the World事件中的一个,这个阶段会标记所有与GC Roots直接相关联的对象,以及被存活的青年代对象所直接引用的对象。

并发标记

GC在运行的过程中用户的应用线程并不会停止工作。该阶段GC收集器会从第一步“初始标记”中所标记出来的对象开始逐步遍历这些对象(与GCRoot直接相连或与存活的青年代对象直接相关联的对象)的所引用的对象,并将这些被引用的对象加上标记。(需要注意的是,这一步中,会漏掉一下老年代的存活对象,这是因为在并发的过程中,用户应用线程可能会对老年代的对象产生引用上的改变。某一些被改变的标记可能会被遗漏。)

并发预清理

并发预清理是Java1.5被加入进来的。主要目的是减少重标记(Remark)步骤Stop-the-World的时间。这一步同样也是并发的,不会停止用户应用线程。在前面的并发标记中,一些引用被改变了。当某一块块(Card)中的对象引用发生改变时,JVM会标记这个空间为“脏块”(Dirty Card)。

在预清理阶段,JVM根据之前记录的这些“脏对象”重新标记了他们新的可达对象。这一步结束后空间重新进入clean状态。另外,一些必要的最终重标记之前的准备步骤也会在这一步做好。

预清理步骤将会不断重复一直到Eden区的占用量达到某个指定的阈值。设定这个阈值作为结束条件的原因主要是为了防止YoungGC产生的Stop-the-World和下一阶段的Remark同时产生,导致系统产生一个更长的停滞。设定了这个阈值之后基本可以保证Remark阶段可以在两次YoungGC之间进行。

重新标记

这是CMS算法中第二个会触发Stop-the-World事件的步骤,由于前一步是一个并发的步骤,预清理的速度可能会赶不上用户应用对对象改变的速度,所以需要一个Stop-the-World的暂停来完整的标记所有对象结束整个标记阶段。

通常CMS会在年轻代为空时来运行重标记阶段,以此避免一个接一个的Stop-the-World阶段。

并发清理

这一阶段程序并发地工作,目的是移除所有不用的对象,并且重新声明内存空间的归属等候将来使用。

这一阶段程序并发地工作,目的是移除所有不用的对象,并且重新声明内存空间的归属等候将来使用。

并发重置

并发地重置所有算法需要的内部数据结构,为下一次GC做准备。

CMS缺点

CMS收集器对CPU资源非常敏感。
在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说是CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量 + 3)/4(-XX:ParallelCMSThreads={x}:设置),也就是当CPU在四个以上时(此时回收一个线程),并发回收时垃圾收集线程不少于25%的CPU资源,并随着CPU数量的增加而下降。但是当CPU不足四个时,CMS对用户程序的影响就可能变得很大,为了应对这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种,其实就是一种抢占式来模拟多任务机制,让多个线程交替运行,尽量减少GC线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会变小,速度下降也就没那么明显了。实践证明,此变种很一般,现在已不再提倡使用。

CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC产生。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在档次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾称为“浮动垃圾”。正是由于垃圾收集过程中,用户线程还要执行,那么也就需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全填满后再进行垃圾收集,需要预留一部分空间提供并发收集时的程序运作使用。可以使用参数-XX:CMSInitiatingOccupancyFraction来设置老年代的阈值,在JDK1.5中设置为当老年代使用了68%就会被激活进行垃圾收集,JDK1.6中设置为92%。要是CMS运行期间预留的内存无法满足程序需要,居会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器重新进行老年代的垃圾收集,此时停顿时间就会很长了。所以该参数设置得太高可能反而会降低性能。

由于CMS是一款基于“标记-清除”算法实现的收集器,在收集结束后会有大量的空间碎片产生。这将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象不得不提前触发一次Full GC。为了解决此问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认是开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发执行的,空间碎片问题没有了,但停顿时间不得不变长。还提供了参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩(不整理)的Full GC后,跟着来一次带压缩的(默认为零,表示每次进入Full GC时都进行碎片整理)。

(5)、G1垃圾收集器

G1将新生代,老年代的物理空间划分取消了,取而代之的是,G1算法将堆划分为若干个区域(Region),区域分为新生代、老年代,它仍然属于分代收集器。

新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。

老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作,避免CMS内存碎片问题。

在G1中,还有一种特殊的区域,叫Humongous区域,专门用于存储巨大对象。如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。

对象分配情况分三种:TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区、Eden区中分配、Humongous区分配。

G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。

Young GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间(Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。)。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。


如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。这样就避免扫描整个老年代。(在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。)


如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(CardTable)。一个CardTable将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。CardTable通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个HashTable,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

Young GC 阶段:

阶段1:根扫描

静态和本地对象被扫描

阶段2:更新RS

处理dirty card队列更新RS

阶段3:处理RS

检测从年轻代指向年老代的对象

阶段4:对象拷贝

拷贝存活的对象到survivor/old区域

阶段5:处理引用队列

软引用,弱引用,虚引用处理

Mix GC

Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

它的GC步骤分2步:

全局并发标记(global concurrent marking)

拷贝存活对象(evacuation)

全局并发标记主要是为Mixed GC提供标记服务的,但并不是一次GC过程的一个必须环节。

STAB(snapshot-at-the-beginning)介绍:

重新标记阶段中,假如重新标记前对象引用如下


这时候应用程序执行了以下操作:A.c=C;B.c=null;这样,对象的状态图变成如下情形:

此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:

在插入的时候记录对象

在删除的时候记录对象

刚好这对应CMS和G1的2种不同实现方式:

在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:

1,在开始标记的时候生成一个快照图标记存活对象

2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)

3,可能存在游离的垃圾,将在下次被收集

这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。

谢谢分享:

http://www.jianshu.com/p/144fe73ad694

http://www.cnblogs.com/ASPNET2008/p/6496481.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容