GC-垃圾收集算法与关键收集器

引言:

在笔者的上两篇博文中,主要介绍了jvm的结构和对象的“生死”问题。今天主要来说说垃圾收集算法与各种关键的收集器,分析比较各种收集算法的优劣。如果时间和篇幅允许的话对内存动态分配做一些解释,因为垃圾回收和动态分配是java的两大基本特性。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

技术点:

1、新生代GC(Minor GC):
上篇博文中,我介绍了新生代,老年代、永久代的概念,新生代的意思就是在这个群体中,具有“朝生夕死”的性质。Minor GC就是对新生代进行垃圾收集。因为新生代的特性,所以MInor GC也会异常频繁。

2、老年代GC(Full GC/Major GC):
Full GC就是对老年代进行收集,收集的频率相对较低,而且收集的时间和速度相对新生代GC来说慢了很多(据说是10倍以上)。

3、GC停顿
指在垃圾回收的过程中,要停止所有的用户线程。为了防止GC过程中对象的引用还在发生变化。换句话说就是在你打扫房间的时候,不允许别人继续扔垃圾了。但是,这里还是要说明一下,很多知道垃圾回收的小伙伴会说在CMS收集器(后面会讨论到)的时候几乎不发生停顿(GC操作可与用户线程并发)啊?其实不然,再讨论生死那篇博文中,我说过“可达性分析算法”判断对象是否存活,在枚举根节点的时候还是会发生停顿的。

4、安全点和安全区域
意思就是当程序跑到这个安全点或者安全区域内才能发生垃圾回收。这样的话系统的性能和数据安全性都能得到比较好的保障。

5、分配担保机制
是指在内存分配中,如果按照复制算法(下面会说道)进行分配内存的时候,当回收的时候发现把某一块的存活对象拷贝到另外一块的时候发现另外一块的内存不足,这个时候就会触发分配担保机制,这些仍旧存活的对象会直接分配到老年代。也就是说你向银行贷款需要有个担保人是一样的,如果你无力偿还的时候,需要担保人进行偿还。

回收算法

在介绍回收算法的时候我会比较一下各个算法的优劣:

1、标记清除算法(Mark-Sweep):
最基础回收算法之一。分为两步,第一步先标记出要回收的对象,然后清除所有被标记的对象。这个算法很好理解,但是它存在着极大的缺陷:①效率不高;②因为如此的回收方式,会打乱内存对象本有的编排顺序,容易造成内存碎片和连续的内存不足问题。比如说本来你左边有一张桌子(一块内存),本来坐着一个200斤的汉子(一个对象),然后这个汉子离开了(被清除),后来又过来一个妹子坐了你左边(新对象进入),然后那个位置对于新来的妹子来说大了太多,有很多空间她都不需要(产生内存碎片),这样一来空间就浪费了。这个时候呢,你右边的位子本来坐了一个80斤的妹子(一个对象),后来这个妹子走了(对象被清除),后来来了一个200斤的汉子(新对象进入),发现完全坐不下了(没有足够大的内存给新对象分配),这个你可能就需要说“朋友,你去那边坐吧,那边空(又触发了一次GC操作)”。不知道这么说,大家能理解么?

2、复制算法(copying):
最基础算法之一。主要是将可用内存划分为大小相等的两块。在使用的过程中就只使用其中的一块,如果使用的这一块被使用完,在GC的过程中把存活对象赋值到另一块上去,并清理原来那块。比如说上课的时候上一半,人满坐不下,老师说大家去隔壁教室上课(GC操作),然后有些爱逃课的小伙伴就乘机溜走了(可回收的对象清除),在新的教室大家按顺序一个个坐下来(把存活的对象都复制到另一块内存),大家座位之间的空位也很合理(没有了空间碎片和不足的问题)。复制算法的缺陷就在于如果按这么操作,那么就会有很大部分的内存在一定时间会被置空,使用率比较低。当然了,在jvm中对于这种的做法是分为1个Eden区和两个Survivor区,两者时间内存大小默认为8:1。对象进来先存在Eden区,回收的时候回把存活的对象拷贝到另外一块Survivor区中(有人会问那还有一块Survivor呢?是这样的,两个Survivor只是进行了切换的操作,它没有座位新来对象的存储,他只是作为在GC复制的时候进行对象转换的作用)。如果在copy的过程中发现另外一块Survivor的内存不足,则就会触发分配担保机制

3、标记整理算法(Mark-Compact):
建立在标记清除算法的基础之上,在标记操作执行之后,会让仍旧存活的对象都向着一端移动,然后清理掉仍旧存活对象边界以后的数据。

4、分代收集算法
根据对象所属的年龄(也就是新生代和老年代)进行不同的收集,比如说新生代“朝生夕死“的性质就可以用复制算法进行收集,而老年代中对象存活的时间就比较长,那就需要用标记清除或者标记整理来进行回收。

垃圾收集器

下图是所有的垃圾收集器,横线上边为新生代收集器,横线下边为老年代收集器,横线上表示既能收集新生代也能收集老年代。各个收集器之间的链接表示此两者可以共同工作。作为垃圾收集器必须要配合新生代收集和老年代收集共同使用。

这里写图片描述

1、Serial收集器:
新生代收集器,采用复制算法进行垃圾收集。最古老的收集器,Serial表示串行的意思,也就是说该收集器是一个单线程的收集器。它在进行垃圾收集是必须暂停其他所有的用户线程,GC停顿感很强。但是单线程收集效率很高,它不用考虑线程交互,专心收集垃圾。

2、ParNew:
新生代收集器,采用复制算法进行垃圾收集。该收集器可以多线程进行收集垃圾,相当于Serial收集器的多线程版本。

3、Parallel Scavenge收集器:
新生代收集器,采用复制算法进行垃圾收集。它也是可以进行多线程收集垃圾,但是它多了一个独特的能力,引入了一个吞吐量(吞吐量=用户代码执行时间/(用户代码执行时间+GC所用时间))的概念,被称为吞吐量优先收集器。它可以自动调节内存分配。

4、Serial Old收集器
老年代收集器,采用标记整理算法进行垃圾收集。和Serial收集器一样是一个单线程收集器。

5、Parallel Old收集器
老年代收集器,采用标记整理算法进行垃圾收集。是一个多线程垃圾收集器。

6、CMS收集器(Concurrent Mark Sweep):
老年代收集器,采用标记清除算法进行垃圾收集。在前面提到过,它的GC停顿时间非常短,它主要有4个步骤进行垃圾收集:①初始标记;②并发标记;③重新标记;④并发清除。这里我再描述一下每个状态的具体情况。在初始标记的时候会产生GC停顿,它是单线程标记。在并发标记的过程中不会产生GC停顿,它可以与用户操作线程进行并发,并不影响用户操作,它主要是寻找引用链(在谈论生死的博客中有提到)的根节点。在重新标记的时候会产生GC停顿,它可以并行操作,主要是修正前面的标记过程中又变化的对象引用。再并发清理阶段,可以与用户操作线程并发处理,不产生GC停顿。仔细看我上面的描述,你会发现有并发和并行两种概念,切不可混为一谈。并发是GC线程与用户线程同时操作,而并行的意思是各个GC线程可以同时操作。并发不存在GC停顿,而并行存在GC停顿。

7、G1收集器
据说是迄今为止最牛逼的收集器。它的GC停顿时间也非常短,主要有4个步骤:①初始标记;②并发标记;③最终标记;④筛选回收。在初始标记中会产生GC停顿,单线程进行标记。在并发标记中不会产生GC停顿,与用户线程并发操作。在最终标记中,GC线程并行处理,会产生GC停顿。修正前面标记过程中导致对象应用又变化的部分。在筛选回收会产生GC停顿,并行处理。

限于篇幅,GC收集器就写这么多。如果各位看官有兴趣,可以针对每一个收集器进行深入探究。

对象分配内存

前面引言中提到,内存动态分配与垃圾回收机制是java的两大特性。简单说一下对象分配的一些规则:

1、前面在说复制算法的时候提到了Eden区,这个区具有优先分配权利。如果分配在这个区中的内存不足以新对象入住就会发生一次Minor GC操作。

2、如果新进入的对象所需连续的内存非常之大(典型的就是数组),那么这个对象可能直接进入老年代中。

3、如果在回收机制进行多次回收之后,那些仍旧存活的对象将会进入老年代。有人会说,那么jvm怎么会知道这个对象存活了几次GC呢?是可以知道的,在执行复制清理算法的时候,存活对象每移动一次,它的年龄就会+1,当年龄达到一定程度后就会放入老年代中。

好啦,总结一下这几天写的,都是比较理论的东西。貌似很多小伙伴都不感兴趣。我想明天是不是要换换口味了,要不明天我们一起研究一下ConcurrentHashMap的数据结构和底层原理?

如果博文存在什么问题,或者有什么想法,可以联系我呀,下面是我的微信二维码:
这里写图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章