jvm垃圾回收算法

355 查看

[TOC]

引用计数

  1. 一般来说,每个对象对应一个计数器,创建对象时,将其计数器置0

  2. 当对象呗赋予任意变量时,引用计数器每次加1.

  3. 引用变量出了作用域后,该引用变量所引用的对象的计数器减1.

  4. 一旦引用计数器为0,对象就满足垃圾收集的条件
    <br/>

  • 优点:基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜必须实时运行的程序。

  • 缺点:引用计数器增加了程序执行的开销,因为每次对象赋予给新的变量,计数器加1,而每次引用变量出了 作用域后,该引用的对象的计数器减1

还无法解决环形引用

Class A(){
    public B b = null;
}
Class B(){
    public A a = null;
}
public void test(){
    A a = new A();
    B b = new B();
    a.b=b;
    b.a = a
}

标记-清除

  1. mutator:除了垃圾收集器之外的部分,比如说我们的应用程序本身。职责(分配内存),read(从内存中读取内容),write(将内容写入内存)

  2. collector:回收不再使用的内存,供mutator 使用new分配内存

  3. mutator roots:一般指的是分配在堆内存之外,可以直接被mutator直接访问到的对象

  • 标记阶段:collector从mutator根对象开始进行遍历,能被mutator roots访问到的对象都打上一个标识,记录为可达对象

  • 清除阶段:collector对内存从头到尾进行线性变量,如果某个对象没有标记为可达对象,就将其回收

从上图我们可以看到,在Mark阶段,从根对象1可以访问到B对象,从B对象又可以访问到E对象,所以B,E对象都是可达的。同理,F,G,J,K也都是可达对象。到了Sweep阶段,所有非可达对象都会被collector回收。同时,Collector在进行标记和清除阶段时会将整个应用程序暂停(mutator),等待标记清除结束后才会恢复应用程序的运行
<br/>

  • 缺点:标记-清除算法的比较大的缺点就是垃圾收集后有可能会造成大量的内存碎片,像上面的图片所示,垃圾收集后内存中存在三个内存碎片,假设一个方格代表1个单位的内存,如果有一个对象需要占用3个内存单位的话,那么就会导致Mutator一直处于暂停状态,而Collector一直在尝试进行垃圾收集,直到Out of Memory。暂停整个应用


复制收集

将堆内存分成两个相同空间,从根(类似于前面的有向图起始顶点)开始访问每一个关联的可达对象,将空间A的全部可达对象复制到空间B,然后一次性回收空间A。对于该算法而言,因为只需访问所有的可达对象,将所有的可达对象复制走之后就直接回收整个空间,完全不用理会不可达对象,所以遍历空间的成本较小,但需要巨大的复制成本和较多的内存。

<br/>

  • 缺点:需要两倍内存空间。

  • 优点:不会出现碎片


标记-压缩

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
<br/>

  • 优点:避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

  • 缺点:暂停整个应用


增量收集

基础仍是传统的标记-清除和复制算法。设计一个多进程的运行环境,比如用一个进程执行垃圾收集工作,另一个进程执行程序代码。这样一来,垃圾收集工作看上去就仿佛是在后台悄悄完成的,不会打断程序代码的运行。
<br/>

  • 缺点:垃圾收集器在第一阶段中辛辛苦苦标记出的结果很可能被另一个进程中的内存操作代码修改得面目全非,以至于第二阶段的工作没有办法开展。---------解决办法: 优化算法

  • 优点:垃圾收集工作看上去就仿佛是在后台悄悄完成的,不会打断程序代码的运行。


分代收集

  • 新生代
    新生代包括两个区:Eden区和Survivor区,其中Survivor区一般也分成两块,简称Survivor1 Space 和 Survivor2 Space (或者From Space 和 To Space)。新生代通常存活时间较短,因此基于标记清除复制算法来进行回收,扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和From或To之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到Survior,最后到旧生代。

回收机制:复制回收

  • 老年代
    在垃圾回收多次,如果对象仍然存活,并且新生代的空间不够,则对象会存放在老年代。

在老年代采用的是 标记清除压缩算法。因为老年代的对象一般存活时间比较长,每次标记清除之后,会有很多的零碎空间,这个就是所谓的浮动垃圾。当老年代的零碎空间不足以分配一个大的对象的时候,就会采用压缩算法。在压缩的时候,应用需要暂停。

回收机制:标记-压缩

  • 持久代
    这部分空间主要存放java方法区的数据以及启动类加载器加载的对象。这一部分对象通常不会被回收。所以持久代空间在默认的情况下是不会被垃圾回收的。

回收机制:不会被回收

首先想eden区申请分配空间,如果空间够,就直接进行分配,否则进行一次Minor GC。minor GC 首先会对Eden区的对象进行标记,标记出来存活的对象。然后把存活的对象copy到From空间。如果From空间足够,则回收eden区可回收的对象。如果from内存空间不够,则把From空间存活的对象复制到To区,如果TO区的内存空间也不够的话,则把To区存活的对象复制到老年代。如果老年代空间也不够(或者达到触发老年年垃圾回收条件的话)则触发一次full GC。
Minor GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就好触发Minor GC,堆Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此应该尽可能减少Full GC。有如下原因可能导致Full GC