Java GC 相关总结

GC的两种判定方法

引用计数 与 可达性分析

  1. 引用计数: 很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。
    引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。

  2. 可达性分析(引用链)
    在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。

实例

在Java语言里,可作为GC Roots对象的包括如下几种:
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象


GC的三种收集方法

标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?

(1)标记清除算法
分为标记和清除两个阶段,首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它主要有两个问题:一个是效率问题,标记清除的效率较低;第二个是碎片问题,清除后会产生大量不连续的内存碎片。
(2)复制算法—回收新生代,回收比例较大
原理就是把内存划分为大小相等的两块,每次只使用其中的一块。当这一块上的内存用完了,就把还存活的对象(可达性分析)复制到另一块上,然后把已使用过的内存空间一次性清理掉,这样每次都不用考虑内存碎片的情况,并且实现的更加简单。如果存活的比例很低,那么复制的操作就很小,效率会比较高。只是如果一次只使用其中一半,那么代价太大了。
现在主要都是用于新生代的回收,98%的新生代对象都是很快就不用的,所以只需要把内存分为一块较大的Eden空间和两块Survivor空间,每次只使用Eden和一块Survivor空间,当回收时把两个区域存活的对象复制到那一块剩余的Survivor上。一般比例为8:1,这样浪费的空间只有10%。如果Survivor内存不够,就是用老年代的内存进行担保。
(3)标记整理算法—回收老年代,回收比例较小
对于老年代,存活的比例一般会比较高,如果使用复制算法那么复制操作的效率就会比较低。根据老年代的特点,提出了标记整理算法,先标记需要回收的对象;第二部不是进行回收,而是将存活对象都移动向一端,然后直接清除掉边界外的内存。
(4)分代收集算法—常见虚拟机的方式
分代收集算法就是综合2、3,对于堆中的不同区域使用不同的收集算法。


GC收集器有哪些?CMS收集器与G1收集器的特点。

(1)Serial收集器

Serial收集器是最基本的收集器,这是一个单线程收集器,它“单线程”的意义不仅仅是说明它只用一个线程去完成垃圾收集工作,更重要的是在它进行垃圾收集工作时,必须暂停其他工作线程,直到它收集完成。Sun将这件事称之为”Stop the world“。
没有一个收集器能完全不停顿,只是停顿的时间长短。
虽然Serial收集器的缺点很明显,但是它仍然是JVM在Client模式下的默认新生代收集器。它有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比较),Serial收集器由于没有线程交互的开销,专心只做垃圾收集自然也获得最高的效率。在用户桌面场景下,分配给JVM的内存不会太多,停顿时间完全可以在几十到一百多毫秒之间,只要收集不频繁,这是完全可以接受的。

(2)ParNew收集器

ParNew是Serial的多线程版本,在回收算法、对象分配原则上都是一致的。ParNew收集器是许多运行在Server模式下的默认新生代垃圾收集器,其主要在于除了Serial收集器,目前只有ParNew收集器能够与CMS收集器配合工作。

(3)Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代垃圾收集器,其使用的算法是复制算法,也是并行的多线程收集器。
Parallel Scavenge 收集器更关注可控制的吞吐量,吞吐量等于运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。
停顿时间越短就越适合需要与用户交互的程序;而高吞吐量则可以最高效的利用CPU的时间,尽快的完成计算任务,主要适用于后台运算。

(4)Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与Serial收集器一样。

(5)Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。

(6)CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短停顿时间为目标的收集器,CMS收集器采用标记--清除算法,运行在老年代。主要包含以下几个步骤:
• 初始标记
• 并发标记
• 重新标记
• 并发清除
其中初始标记和重新标记仍然需要“Stop the world”。初始标记仅仅标记GC Root能直接关联的对象,并发标记就是进行GC Root Tracing过程,而重新标记则是为了修正并发标记期间,因用户程序继续运行而导致标记变动的那部分对象的标记记录。
由于整个过程中最耗时的并发标记和并发清除,收集线程和用户线程一起工作,所以总体上来说,CMS收集器回收过程是与用户线程并发执行的。虽然CMS优点是并发收集、低停顿,很大程度上已经是一个不错的垃圾收集器,但是还是有三个显著的缺点:
• CMS收集器对CPU资源很敏感。在并发阶段,虽然它不会导致用户线程停顿,但是会因为占用一部分线程(CPU资源)而导致应用程序变慢。
• CMS收集器不能处理浮动垃圾。所谓的“浮动垃圾”,就是在并发标记阶段,由于用户程序在运行,那么自然就会有新的垃圾产生,这部分垃圾被标记过后,CMS无法在当次集中处理它们,只好在下一次GC的时候处理,这部分未处理的垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段程序还需要运行,即还需要预留足够的内存空间供用户使用,因此CMS收集器不能像其他收集器那样等到老年代几乎填满才进行收集,需要预留一部分空间提供并发收集时程序运作使用。要是CMS预留的内存空间不能满足程序的要求,这是JVM就会启动预备方案:临时启动Serial Old收集器来收集老年代,这样停顿的时间就会很长。
• 由于CMS使用标记--清除算法,所以在收集之后会产生大量内存碎片。当内存碎片过多时,将会给分配大对象带来困难,这是就会进行Full GC。

(6)G1收集器

G1收集器与CMS相比有很大的改进:
• G1收集器采用标记--整理算法实现。
• 可以非常精确地控制停顿。
G1收集器可以实现在基本不牺牲吞吐量的情况下完成低停顿的内存回收,这是由于它极力的避免全区域的回收,G1收集器将Java堆(包括新生代和老年代)划分为多个区域(Region),并在后台维护一个优先列表,每次根据允许的时间,优先回收垃圾最多的区域 。


Minor GC与Full GC分别在什么时候发生?

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。对于一个拥有终结方法的对象,在垃圾收集器释放对象前必须执行终结方法。但是当垃圾收集器第二次收集这个对象时便不会再次调用终结方法。
(1)Minor GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区,然后整理Survivor的两个区。这种方式的GC是对新生代的Eden区进行,不会影响到老年代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。
(2)Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
• 老年代(Tenured)被写满
• 持久代(Perm)被写满
• System.gc()被显示调用

推荐阅读更多精彩内容

  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供...
    简欲明心阅读 31,346评论 15 244
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 5,991评论 3 62
  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 3,348评论 2 24
  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 830评论 0 7
  • 来自: Android梦想特工队作者: Aaron主页: http://www.wxtlife.com/原...
    技术特工队阅读 1,689评论 0 26