JVM篇之 GC算法

–引用计数法

是一个老牌垃圾回收算法,通过引用计算来标记,判断一个对象是不是垃圾,是不是要回收。

基本思想 : 为每一个对象都标记一个引用数量,引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

引用计数

对于上图,根对象和ABC三个对象,有互相引用的关系,ABC引用计数分别为1

当A对B的引用失去之后,B的引用变成了0,根据引用计数的规则,引用为0 的B对象就会当作垃圾回收掉,回收之后堆中只剩下根对象和AC。

引用计数法带来的问题

1、引用和去引用伴随加法和减法,,因为要实时的计算当前对象有多少个引用,对象的引用和消是无时无刻存在的,所以对影响有一定性能

2、很难处理循环引用的问题,看下面的问题,
循环引用
上图中,根对象引用A,引用C,C引用B,B又引用A,所以引用计算A(2),C(1),B(1),当,根对象失去对A的引用时,由于A,B,C三者互相引用,所以三个对象的引用计数都是1,所以A,B,C相当于一个独立体,无法被回收,如果堆中存在大量这样的无用对象,就会导致不停的GC,而GC后又无效果,最终内存溢出

–标记清除

标记-清除算法是现代垃圾回收算法的思想基础。    

基本思想:标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点做搜索,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

标记清除
在标记清除算法中,大致总结思路如下:从根节点开始做两件事情

1、标活(标记可达对象),标记A,B,C,D,E是可达对象

2、去死 (去除不可达对象),去除黑色区域不可达对象

通过上面图可以反应出,从堆中清空垃圾对象后,堆中的可用空间比较分散不连续,这时候如果有一个大的对象需要申请6个连续的空间,是无法分配,会造成大量的空间碎片。标记压缩算法可以避免这种情况的发生。

–标记压缩

基本思路:在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

执行过程如下图:
标记压缩
标记压缩
大致总结思路如下:从根节点开始做两件事情

1、标活

2、移动存活对象到一端

3、清除边界外的所有空间

标记压缩对标记清除而言,有什么优势呢?

相比与标记清除算法,清理之后,会有大量的连续空间,不会有空间碎片,标记-压缩算法适合用于存活对象较多的场合,如老年代。

–复制算法

基本思想 :将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

复制算法两空一样大小的空间,下面是复制算法的执行过程:
标记压缩
复制算法
1、标活

2、复制存活对象从FROM区域到TO区域

3、交换FROM与TO的角色

与标记-清除算法相比,复制算法是一种相对高效的回收方法,在现在的JVM中,复制算法一般会用于年轻代,但是复制算法的最大问题是:空间浪费,每次只使用一半的空间。

可以想想,比如为复制算法分配8G的空间,但是在使用过程中会浪费4G的空间,当然8G的复制空间也只是举个例子,为了更能突出空间的浪费 ,因为复制算法本身的特性决定 ,是需要将存活对象进行一次复制 ,所以复制空间一般都不会很大,因为大的空间,如果在极端情况下,会涉及大量存活对象的移动,这种大量的移动对于系统本身,无疑问是灾难性的。

在现在的JVM中也不会很大,每一块复制区域默认占用1/10的空间。比如,有10M的空间默认的,FROM和TO区域分别占用1M。

所以一般整合标记清理思想,对复制算法会做一些改进,如下图:
标记压缩
复制算法改进
因为复制空间越大,浪费的空间越大,复制算法空间一般不会很大,一般在复制过程 中会了现两种情况

第一种:对象太大,放不到复制空间

第二种:即使大对象放得下,也会导致大量的小对象无法存放,会导致大量的不符合老年代特性的小对象进行老年代

所以复制算法一般需要一个担保空间,如上图中蓝色区域。

比如:C、D、F是大对象,在一执行复制算法后,会直接进入到担保空间。

对于对JVM堆内存划分有了解的人,可能对上面的图比较熟悉,这其实已经很接近JVM堆空间的结构,蓝色区域复制算法的担保空间就我们经常说的老年代(Old Generation)。顶部的区域就是年轻代(Eden),而中间两块复制算法的空间就是幸存区(Surivor),对比上面和下面的图,发现是完全可以吻合的。

JVM堆内存划分

标记压缩

分代思想

依据对象的存活周期进行分类

第一类:有些对象创建后,很快就会回收,我们把短命对象归为新生代,

第二类:有些对象创建后会长期存在,有可能和JVM生命周期相同,我们把长命对象归为老年代。

根据不同代的特点,选取合适的收集算法

–少量对象存活,适合复制算法

–老年代中的对象有两种:

老年代中的对象:多次GC没有回收掉,年龄校大的对象,生命周期较长

复制算法空间担保 ,直接进入 老年代的大对象

所以会有大量对象存活,和大对象,适合标记清理或者标记压缩

所有的算法,需要能够识别一个垃圾对象,因此需要给出一个可触及性的定义

什么是可触及的:从根节点可以到达的对象,此对象称为可触及的

可复活的: 一旦所有引用被释放,就是可复活状态(因为在finalize()中可能复活该对象)

什么是不可触及的:在finalize()后,可能会进入不可触及状态,不可触及的对象不可能复活,不可触及的对象,是可以回收的

上面的算法中,我们一直说从根节点查找,都提到了根节点,所以我们要搞清楚什么是根节点?

–栈中引用的对象(局部变量表)

–方法区中静态成员或者常量引用的对象(全局对象)

–JNI方法栈中引用对象

根是一系列对象的集合,是一砣根节点,文章中为了简单所以只有一个根对象,希望不要给大家带来误解!

GC中的一个重要 的现象Stop-The-Word

java中一种全局暂停的现象,所有的JAVA代码停止 ,native代码可能执行,但不能和JVM交互

引起STW的原因 :GC 、DUMP线程、堆 DUMP

多半是因为GC引起的。

为什么会产生全局停顿?

GC需要一个安静的状态来完成垃圾的回收,所以需要将用户线程停止,完成垃圾回收后,再继续用户线程。

STW危害?

1、影响系统吞吐量,系统响应时间 增长

2、长时间的服务停止,没有响应,遇到HA系统,可能引起主备切换,最终导致主备同时启动。在一些业务场景下,可能会导致数据不致,甚至无法正常工作。

大家好我是BK,第一次写文章 ,写和不是很好,有错误的地方,请大家指正,谢谢大家!

后续会更新GC的种类和各类的区别。希望大家可以关注我。

BK wechat
扫一扫,用手机访问本站
---------------- 本文结束 ----------------