垃圾回收相关算法

返回主博客

目录

标记阶段

引用计数算法

可达性分析算法

MAT与Jprofiler的GC Roots溯源

清除阶段

标记清除算法(Mark-Sweep)

复制算法

标记压缩算法

分代收集算法

增量收集算法

分区算法


标记阶段

标记哪些对象已经死了,当一个对象不再被任何存活对象引用时,宣布死亡。

一般有两种标记算法:

  1. 引用计数算法
  2. 可达性分析算法

引用计数算法

对每个对象保存一个整型的引用技术属性。用于记录对象被引用的次数。对象被谁引用就+1,引用失效就-1。

有点:

  • 简单实现,便于辨识,判断效率高,回收没有延迟。

缺点

  • 需要单独的字段存储计数器,这样做法增加了存储空间的开销。
  • 每次引用和引用失效都需要更新计数器,增加时间开销。
  • 致命问题是无法处理循环引用问题。

由于无法解决循环依赖问题,没有被java考虑使用。、

但是在python中会使用引用计数算法,但是他会解决循环依赖问题

  • 手动解除,在合适的时机接触引用关系
  • 使用弱引用weakref,weakref是python提供的标准库,旨在解决循环引用

 

可达性分析算法

相对引用计数算法,可达性分析算法,同样具备简单实现和执行高效,更重要的是可以解决循环依赖。

基本思路:

以根对象集合(GC root)为起始点从上到下搜索被根节点对象所连接的目标对象是否可达。

使用可达性分析算法,存活的对象都会直接或间接得被GC root引用到。

在java语言中GC root 包含一下几类元素。

  • 虚拟机栈中引用的对象,各个线程被调用的方法用到的参数,被局部变量引用。
  • 本地方法栈(JNI)中引用的对象。
  • 方法区中类的静态属性(1.7后和Class对象一起放到堆空间)引用的对象。
  • 方法区中常量引用的对象。比如String Table里的引用。
  • 被同步监视器(synchronized)持有的对象
  • java虚拟机内部的引用。比如基本数据类型对应的Class对象,常驻的异常对象,系统类加载器等。
  • 反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等

总结来说GCroot的引用存在于堆空间之外,如图。

注意

如果minorGC 时,也可以考虑将老年代对象作为GC root。

如果使用可达性分析算法,那么分析工作必须在一个能保证一致性的快照中进行。这点也是导致GC进行时必须STW的原因。即使好处不会发生停顿的CMS收集器,枚举根节点的时候也是必须要停顿的。

如果不STW就会有问题,比如C本来被B引用,GC线程分析A,的时候判断C没有被A引用,刚刚判断完,用户线程又把C交给A,把B 到 C断开,这样,扫描B的时候又认为没有被B引用。这样就导致C被标记为死了。

 

对象的finalization机制

在回收对象之前总会调用该对象的finalize方法。这个方法交给用户自定义实现。可以用于资源释放。

永远要主动调用finalize方法,应该交给垃圾回收器。因为:

  1. 在finalize()时可能导致对象复活。
  2. finalize()方法的执行时间时没有保障的,它完全由GC线程决定。
  3. 糟糕的finalize方法会严重影响GC性能。其实也不建议重写finalize方法。

从功能上来看,finalize()方法和C++的析构函数比较类似。但是不同于析构函数。因为java对象的释放由GC管理。

由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态:

  1. 可触及,从根节点开始可以被触及。
  2. 可复活,在finalize方法中,被刀下留人了
  3. 不可触及,不能被根节点触及。

判断一个对象objA是否可以被回收至少经历两次标记过程。

1、如果从objA到 GC Root没有引用恋,则进行第一次标记

2、进行筛选,判断对象有无必要执行finalize方法。

如果finalize没有被重写就不执行,便不会执行,对象判定为不可触及

如果对象的objA重写了finalize且未执行,那么objA会被插入到F-Queue的队列中,由一个虚拟机自动创建的,优先级的Finalizer线程触发器finalize方法。

3、finalize方法是对象死里逃生的最后机会,如果在finalize中使对象被引用链上任何一个对象引用。那么在第二次标记后,对象则变成可复活状态,反之则标记为不可触及

代码演示:我们可以看到finalize只会被调用一次,第二次gc的时候,它不再有复活机会。

public class FinalizeTest {
    public static FinalizeTest obj;
    @Override
    protected void finalize() throws Throwable {
        obj = this;
    }
    public static void main(String[] args) throws InterruptedException {
        obj = new FinalizeTest();
        obj = null;
        System.gc();
        System.out.println("第一次gc");
        Thread.sleep(2000);
        if (obj == null) {
            System.out.println("obj is dead");
        } else {
            System.out.println("obj is still alive");
        }
        obj = null;
        System.gc();
        System.out.println("第二次gc");
        Thread.sleep(2000);
        if (obj == null) {
            System.out.println("obj is dead");
        } else {
            System.out.println("obj is still alive");
        }
    }
}
/** 执行结果:
第一次gc
obj is still alive
第二次gc
obj is dead
*/

MAT与Jprofiler的GC Roots溯源

这一节可以先不看,都是实操,我也懒得写了。

MAT (Memory Analyzer)一款功能强大的java堆内存分析器,用于查找内存泄漏以及内存消耗情况。

下载地址:http://www.eclipse.org/mat/

获取dump文件

-XX:+HeapDumpOnOutOfMemoryError 设置OOM时生成dump文件。

 

 

清除阶段

区分出存活对象和死亡对象后,接下来就是执行垃圾回收。

标记清除算法(Mark-Sweep)

 

最早用于Lisp语言

过程:

标记:从根节点开始,标记所有被引用的对象。

清除:遍历堆空间,将没有标记存活的对象进行回收(用空闲列表标记这些被回收的地址,代表这些块是空闲的)。

 

优点

  • 简单,容易想到

缺点

  1. 效率不高,需要递归遍历标记,清除的时候还会遍历堆空间
  2. GC的时候需要STW,影响用户体验
  3. 存在内存碎片,内存利用率下降。需要维护一个空闲列表(用于存储很多可用内存的起始地址和结束地址)

何为清除:

清除不是真的置空,而是把释放的内存块的地址保存到空闲列表。后面应用程序就认为他是空闲的,下次存放覆盖它。

所以我们要注意:”标记清除算法" 的”标记“两个字是指将空闲区域标记出来,维护一个空闲列表,用于存储那些可用内存的起始地址和结束地址)表示它们是空闲的,下次可以直接使用。而不是指我们GC算法的标记阶段的“标记”。

 

适合场景(比如eden区)

  • 存活对象较少,回收后能让我们的 “空闲列表”短且每个内存块大。
  • 因频繁回收而要求速度很快

 

复制算法

为了解决标记清除算法的缺陷,复制算法被提出。

核心思想:如下图,将活着的内存空间分成两块,每次只使用其中一块。回收时将其中一块的内存内容连续地复制到另外一个区。surviver区用的就是这个算法。

优点:

  1. 不需要维护空闲列表
  2. 没有内存碎片
  3. 下次分配内存时只需要指针碰撞(只需要记录已用空间和空闲空间的分界指针即可),按顺序存放即可

缺点:

  • 有一半空间不可使用
  • 每次还要对内容进行复制
  • 地址被改变,需要修改原来的引用它的地址值

适合场景(比如surviver区的需求。新生代的对象大多朝生夕死的)

  • 那种存活对象数量很少
  • 区域小
  • 并且对象不会很大的场景。

 

标记压缩算法

边标记边压缩(当然它也可以标记完再压缩),遍历堆空间,遇到非存活内存时将其标记起来,表示他们是空闲区域,遇到非空闲内存则将其压缩到空闲区域,并更新“标记”的起始地址。

优点

  • 内存利用率高,
  • 下次配分对象可以指针碰撞(只需要记录已用空间和空闲空间的分界指针即可)按序分配。

缺点:

  • 压缩耗时
  • 移动对象地址时需要复制
  • 地址被改变,需要修改原来的引用它的地址值

 

适合放置大对象,不经常会回收的区,比如老年代。

 

思考

其实对于垃圾的回收,不必使JVM总是处于STW状态,比如老年代的标记压缩算法,比如在进入magerGC的时候,我们可以记录一下,当前是否正在进行mager GC,这时线程可以边标记边压缩,新对象需要分配到老年代的时候,停止“边标记边压缩”的动作,根据新对象的大小将新对象分配到一个合适的位置后做些特殊处理(指操作那些记录GC信息,比如空闲区域的起始地址),接着继续进行。(见下一节增量算法)

 

小结

 Mark-SweepCopyingMark-Compact
速度中等最快最慢
空间较少(存在碎片)少(不存在碎片)多(一半可用)
移动对象
下次分配不方便方便方便

分代收集算法

没有最优的清除算法,只有最合适的算法,因此我们分代选择不同算法。

因此我们在新生代一般使用标记清除算法,为了使其更高效一点配合了复制算法,在eden区使用标记清除算法,survicer区使用复制算法。在老年代使用标记压缩算法。

 

增量收集算法

如果只考虑上面三种算法,GC过程中,程序将处于STW状态,尤其是堆空间越大,STW时间就越长,影响用户体验。为了减少STW时间,提出增量收集算法。

其实对于垃圾的回收,不必使JVM总是处于STW状态,我们可以边回收便分配。

增量搜集算法,就是是在使用上面三种算法回收内存的同时,回收一片区域后,可以切换到用户线程,然后再切回GC线程回收。将用户线程和GC线程混搭起来。但是这个过程是相当复杂的,我们需要考虑会遇到问题,如何解决解决这些问题。

比如:

  • 比如一个对象创建时要判断当前JVM是否再进行GC。
  • 当一个对象进入到enden,surviver,或者老年代的时候,我们如何选择一个合理的地址。比如我们在前面提到的“思考”。
  • 当用户创建新的对象,我们如何将其和其他处于GC中的对象区分开来。

等等问题。

我们对垃圾回收机制的优化,主要就是为了减少STW的时间,要混搭使用是要考虑很多问题的,主要就是并发问题,有些问题可能还会考虑不到,这是一个不断实践,发现,解决和测试的过程。

增量收集算法只是了解和思考一下,叫我讲我也讲不好,前面几个问题也是我自己的思考,大家也可以自己思考思考。增量收集算法是再上面三种算法的补充。

分区算法

堆空间越大,我们GC的频率越低,但是由于每次GC的次数就越多。好比我们有很多衣服,我们一个月洗一次,虽然洗的次数少,但是洗的时候耗时长。出来混总是要还的,衣服再多也要洗。不管你怎么优化,洗衣服的总时间还是一样的。

为了使在堆空间很大的时候,使我们减少我们每次GC的时间,我们提出分区算法。

将一个很大的内存区域分割成多个小块,根据目标的停顿时间,每次合理回收若干个小区间(region)。

区别于分代算法。

分代算法是将堆空间分成三个代,不同代使用不同算法。而分区是指将堆空间分成若干region,这些region都是独立GC的。

如下图是G1的分区结构

思考,分区算法也是很复杂的,我们如何分区是一个问题,因为,对象之间的引用是不可预知的,A分区的对象可以引用B分区的对象。

 

对分区算法和增量算法再次思考

我们在标记对象是否存活时可以有如下选择:

1、判断死亡:所有对象默认存活,然后遍历分区中的对象,在GC root的引用链中过一遍,判断它是否死亡。在finalize调用之后再遍历分区再遍历GCroot引用链标记一次,如何在遍历分区进行回收,期间至少5次遍历。

2、判断存活:所有对象默认死亡,然后直接遍历GCroot 引用链,标记对象是否存活,finalize之后再遍历一次,清除阶段遍历分区一次,期间至少三次。

如果使用1,我们可以明确知道当前分区的每个对象是否死亡,但是对于2就不一定。因为,标记它是否存活,那么一开始我们就要默认所有对象已经死亡,死亡就代表标记的重置,这个重置动作要在上一次清除阶段完成之后进行,初始化动作要在对象初始化之后完成。那么如果考虑使用户线程能够穿插GC阶段,标记阶段就会引发很多问题。那么我们是否可以考虑将对象加一个状态标识?将新增和移到其他代的对象标记为new?在其经历了清除阶段后将其标记为死亡?这是一个并发问题。这是我的思考。

再思考,如果将新对象标记为new,那么该对象本次GC的时候,就不会死亡。这样会导致,我们的eden区在本次GC后,存活对象稍微多一点。

 

 

 

 

 

 

 

 

 

 

 

 

垃圾回收相关概念

垃圾回收器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值