Java垃圾回收GC(Garbage Collection)

1、垃圾收集的算法分析

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就是正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾回收首先需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。下面介绍几个常用的算法。

1.1. 引用计数法(Reference Counting Collector)

  引用计数法是唯一没有使用根集的垃圾回收的法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。
  基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器加1,而每次现有对象出了作用域生,计数器减1。
  
引用计数法实现简单,判定高效,但不能解决对象之间相互引用的问题。

public class GCtest {
    private Object instance = null;
    private static final int _10M = 10 * 1 << 20;
    // 一个对象占10M,方便在GC日志中看出是否被回收
    private byte[] bigSize = new byte[_10M];

    public static void main(String[] args) {
        GCtest objA = new GCtest();
        GCtest objB = new GCtest();

        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        System.gc();
    }
}

最后两句将objA和objB赋值为null,也就是说objA和objB指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

为了解决这个问题,java采用了可行性分析

可达性分析法

通过一系列称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索路径称为 “引用链”,以下对象可作为GC Roots:

  • 本地变量表中引用的对象
  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • Native方法引用的对象

当一个对象到 GC Roots 没有任何引用链时,意味着该对象可以被回收。
这里写图片描述
在可达性分析法中,判定一个对象objA是否可回收,至少要经历两次标记过程:

  1. 如果对象objA到 GC Roots没有引用链,则进行第一次标记。
  2. 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法。finalize()方法是对象逃脱死亡的最后机会,GC会对队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立联系,那么在第二次标记时,objA会被移出“即将回收”集合。

看看具体实现

public class FinalizerTest {
    public static FinalizerTest object;
    public void isAlive() {
        System.out.println("I'm alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("method finalize is running");
        object = this;
    }

    public static void main(String[] args) throws Exception {
        object = new FinalizerTest();

        // 第一次执行,finalize方法会自救
        object = null;
        System.gc();

        Thread.sleep(500);
        if (object != null) {
            object.isAlive();
        } else {
            System.out.println("I'm dead");
        }

        // 第二次执行,finalize方法已经执行过
        object = null;
        System.gc();

        Thread.sleep(500);
        if (object != null) {
            object.isAlive();
        } else {
            System.out.println("I'm dead");
        }
    }
}

执行结果:

method finalize is running
I'm alive
I'm dead

从执行结果可以看出:
第一次发生GC时,finalize方法的确执行了,并且在被回收之前成功逃脱;
第二次发生GC时,由于finalize方法只会被JVM调用一次,object被回收。

当然了,在实际项目中应该尽量避免使用finalize方法。

1.2. tracing算法(Tracing Collector)

  tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器.
这里写图片描述

1.3. compacting算法(Compacting Collector)

  标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
  这里写图片描述

1.4. copying算法(Coping Collector)

  该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成一个对象区和多个空闲区,程序从对象区为对象分配空间,当对象满了,基于coping算法的垃圾回收就从根集中扫描活动对象,并将每个活动对象复制到空闲区(使得活动对象所占的内存之间没有空闲间隔),这样空闲区变成了对象区,原来的对象区变成了空闲区,程序会在新的对象区中分配内存。
  一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区域区,在对象区与空闲区域的切换过程中,程序暂停执行。
这里写图片描述

1.5. generation算法(Generational Collector)

这里写图片描述
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

新生代(Young Generation)
新生代由 EdenSurvivor Space(S0,S1)构成,大小通过-Xmn参数指定,Eden 与 Survivor Space 的内存大小比例默认为8:1,可以通过-XX:SurvivorRatio 参数指定,比如新生代为10M 时,Eden分配8M,S0和S1各分配1M。

大多数情况下,对象在Eden中分配,当Eden没有足够空间时,会触发一次Minor GC,虚拟机提供了-XX:+PrintGCDetails参数,告诉虚拟机在发生垃圾回收时打印内存回收日志。

Survivor:意思为幸存者,是新生代和老年代的缓冲区域。
当新生代发生GC(Minor GC)时,会将存活的对象移动到S0内存区域,并清空Eden区域,当再次发生Minor GC时,将Eden和S0中存活的对象移动到S1内存区域。

存活对象会反复在S0和S1之间移动,当对象从Eden移动到Survivor或者在Survivor之间移动时,对象的GC年龄自动累加,当GC年龄超过默认阈值15时,会将该对象移动到老年代,可以通过参数-XX:MaxTenuringThreshold 对GC年龄的阈值进行设置。

老年代(Old Generation)
老年代的空间大小即-Xmx 与-Xmn 两个参数之差,用于存放经过几次Minor GC之后依旧存活的对象。当老年代的空间不足时,会触发Major GC/Full GC,速度一般比Minor GC慢10倍以上。

永久代(Permanent Generation)
在JDK8之前的HotSpot实现中,类的元数据如方法数据、方法信息(字节码,栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32位默认永久代的大小为64M,64位默认为85M,可以通过参数-XX:MaxPermSize进行设置,一旦类的元数据超过了永久代大小,就会抛出OOM异常。

虚拟机团队在JDK8的HotSpot中,把永久代从Java堆中移除了,并把类的元数据直接保存在本地内存区域(堆外内存),称之为元空间。

这样做有什么好处?

有经验的同学会发现,对永久代的调优过程非常困难,永久代的大小很难确定,其中涉及到太多因素,如类的总数、常量池大小和方法数量等,而且永久代的数据可能会随着每一次Full GC而发生移动。

而在JDK8中,类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间,可以避免永久代的内存溢出问题,不过需要监控内存的消耗情况,一旦发生内存泄漏,会占用大量的本地内存。

ps:JDK7之前的HotSpot,字符串常量池的字符串被存储在永久代中,因此可能导致一系列的性能问题和内存溢出错误。在JDK8中,字符串常量池中只保存字符串的引用。

2、垃圾收集算法

  在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。

2.1、Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
这里写图片描述

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2.2、Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
这里写图片描述

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

2.3、Mark-Compact(标记-整理)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
这里写图片描述

2.4、Generational Collection(分代收集)算法

  分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

3、典型的垃圾收集器

这里写图片描述

上图展示了7种不同分代的收集器,如果两两之间存在连线,说明可以组合使用。

3.1、Serial收集器(串行GC)

Serial 是一个采用单个线程并基于复制(Copying)算法工作在新生代的收集器,进行垃圾收集时,必须暂停其他所有的工作线程。对于单CPU环境来说,Serial由于没有线程交互的开销,可以很高效的进行垃圾收集动作,是Client模式下新生代默认的收集器。

3.2、ParNew收集器(并行GC)

ParNew其实是serial的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial一样。

3.3、Parallel Scavenge收集器(并行回收GC)

Parallel Scavenge是一个采用多线程基于复制(Copying)算法并工作在新生代的收集器,它在回收期间不需要暂停其他用户线程,其关注点在于达到一个可控的吞吐量,经常被称为“吞吐量优先”的收集器。

吞吐量 = 用户代码运行时间 /(用户代码运行时间 + 垃圾收集时间)

Parallel Scavenge提供了两个参数用于精确控制吞吐量:
1、-XX:MaxGCPauseMillis 设置垃圾收集的最大停顿时间
2、-XX:GCTimeRatio 设置吞吐量大小

3.4、Serial Old收集器(串行GC)

Serial Old 是一个采用单线程基于标记-整理(Mark-Compact)算法并工作在老年代的收集器,是Client模式下老年代默认的收集器。

3.5、Parallel Old收集器(并行GC)

Parallel Old是一个采用多线程基于标记-整理(Mark-Compact)算法并工作在老年代的收集器。在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge和Parallel Old的收集器组合。

3.6、CMS收集器(并发GC)

CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器,工作在老年代,基于“标记-清除(Mark-Sweep)”算法实现,整个过程分为以下4步:

  1. 初始标记:这个过程只是标记以下GC Roots能够直接关联的对象,但是仍然会Stop The World;
  2. 并发标记:进行GC Roots Tracing的过程,可以和用户线程一起工作。
  3. 重新标记:用于修正并发标记期间由于用户程序继续运行而导致标记产生变动的那部分记录,这个过程会暂停所有线程,但其停顿时间远比并发标记的时间短;
  4. 并发清理:可以和用户线程一起工作。

CMS收集器的缺点:

1、对CPU资源比较敏感,在并发阶段,虽然不会导致用户线程停顿,但是会占用一部分线程资源,降低系统的总吞吐量。
2、无法处理浮动垃圾,在并发清理阶段,用户线程的运行依然会产生新的垃圾对象,这部分垃圾只能在下一次GC时收集。
3、CMS是基于标记-清除算法实现的,意味着收集结束后会造成大量的内存碎片,可能导致出现老年代剩余空间很大,却无法找到足够大的连续空间分配当前对象,不得不提前触发一次Full GC。

JDK1.5实现中,当老年代空间使用率达到68%时,就会触发CMS收集器,如果应用中老年代增长不是太快,可以通过-XX:CMSInitiatingOccupancyFraction参数提高触发百分比,从而降低内存回收次数提高系统性能。

JDK1.6实现中,触发CMS收集器的阈值已经提升到92%,要是CMS运行期间预留的内存无法满足用户线程需要,会出现一次”Concurrent Mode Failure”失败,这时虚拟机会启动Serial Old收集器对老年代进行垃圾收集,当然,这样应用的停顿时间就更长了,所以这个阈值也不能设置的太高,如果导致了”Concurrent Mode Failure”失败,反而会降低性能,至于如何设置这个阈值,还得长时间的对老年代空间的使用情况进行监控。

3.7、G1收集器

G1(Garbage First)是JDK1.7提供的一个工作在新生代和老年代的收集器,基于“标记-整理”算法实现,在收集结束后可以避免内存碎片问题。

G1优点:

  1. 并行与并发:充分利用多CPU来缩短Stop The World的停顿时间;
  2. 分代收集:不需要其他收集配合就可以管理整个Java堆,采用不同的方式处理新建的对象、已经存活一段时间和经历过多次GC的对象获取更好的收集效果;
  3. 空间整合:与CMS的”标记-清除”算法不同,G1在运行期间不会产生内存空间碎片,有利于应用的长时间运行,且分配大对象时,不会导致由于无法申请到足够大的连续内存而提前触发一次Full GC;
  4. 停顿预测:G1中可以建立可预测的停顿时间模型,能让使用者明确指定在M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

NOTE:
使用G1收集器时,Java堆的内存布局与其他收集器有很大区别,整个Java堆会被划分为多个大小相等的独立区域Region,新生代和老年代不再是物理隔离了,都是一部分Region不需要连续的集合。G1会跟踪各个Region的垃圾收集情况(回收空间大小和回收消耗的时间),维护一个优先列表,根据允许的收集时间,优先回收价值最大的Region,避免在整个Java堆上进行全区域的垃圾回收,确保了G1收集器可以在有限的时间内尽可能收集更多的垃圾。

不过问题来了:使用G1收集器,一个对象分配在某个Region中,可以和Java堆上任意的对象有引用关系,那么如何判定一个对象是否存活,是否需要扫描整个Java堆?其实这个问题在之前收集器中也存在,如果回收新生代的对象时,不得不同时扫描老年代的话,会大大降低Minor GC的效率。

针对这种情况,虚拟机提供了一个解决方案:G1收集器中Region之间的对象引用关系和其他收集器中新生代与老年代之间的对象引用关系被保存在Remenbered Set数据结构中,用来避免全堆扫描。G1中每个Region都有一个对应的Remenbered Set,当虚拟机发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于相同的Region中,如果不是,则通过CardTable把相关引用信息记录到被引用对象所属Region的Remenbered Set中。

转载:http://www.codeceo.com/article/java-gc-part-one.html
http://www.codeceo.com/article/java-gc-part-two.html
http://www.cnblogs.com/dolphin0520/p/3783345.html
http://blog.csdn.net/zsuguangh/article/details/6429592

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值