JVM垃圾回收原理:标记回收对象,四种引用,垃圾收集算法,垃圾收集器

本文主要为《深刻理解Java虚拟机》第三章的读书记录笔记,同时伴有一些网络上资料的总结。html

1. 标记回收对象-对象已死?

Java堆是JVM主要的内存管理区域,里面存放着大量的对象实例和数组。在垃圾回收算法和垃圾收集器以前,首先要作的就是判断哪些对象已经“死去”,须要进行回收即不可能再被任何途径使用的对象。java

1.1 引用计数法

引用计数法是这样:给对象中添加一个引用计数器,每当有一个地方使用它时,计数器值就加1。当引用失效时,计数器就减1。任什么时候刻计数器为0的对象就是不可能再被使用的。算法

如今主流的Java虚拟机都没有使用引用计数法,最主要的缘由就是它很难解决对象之间互相循环引用的问题数组

1.2 可达性分析

可达性分析的基本思路:经过一系列称为"GC Roots"的对象做为起点,从这些节点开始向下搜索,若是从GC Roots到一个对象不可达,则证实此对象是不可用的,以下图所示。浏览器

image.png

Java语言中,可做为GC Roots的对象包括下面几种:缓存

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 本地方法栈JNI(即通常说的Native方法)引用的对象
  3. 方法区中类静态常量引用的对象
  4. 方法区中常量引用的对象

对于Java程序而言,对象基本都位于堆内存中,简单来讲GC Roots就是有被堆外区域引用的对象。bash

2. 四种引用

JDK 1.2之前的版本中,若一个对象不被任何变量引用,那么程序就没法再使用这个对象。也就是说,只有对象处于(reachable)可达状态,程序才能使用它。网络

JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用软引用弱引用虚引用多线程

2.1 强引用(StrongReference)

强引用是使用最广泛的引用,以下的方式就是强引用:并发

Object strongReference = new Object();
复制代码
  1. 若是一个对象具备强引用,那垃圾回收器绝不会回收它。直到强引用的对象不使用或者超出对象的生命周期范围。则GC认为该对象不存在引用,这时候就能够回收这个对象。
  2. 当内存不足时,JVM宁愿抛出OutOfMemoryError的错误,使程序异常终止,也不会靠随意回收具备强引用对象来解决内存不足的问题。

举例来讲,

  1. 以下图在一个方法内部具备一个强引用,这个引用保存在虚拟栈的栈帧中,而真正的引用内容Object则保存在Java堆中。当这个方法运行完成后,退出方法栈。这个对象再也不被GC Roots可达,那么这个对象在下次GC时就会被回收。
public void test() {
        Object strongReference = new Object();
        // 省略其余操做
    }
复制代码
  1. 以下图一个类的静态变量须要一个强引用,这个引用保存在方法区中,而真正的引用内容Object则保存在Java堆中。当将这个引用手动制空strongReference = null后。这个对象再也不被GC Roots可达,那么这个对象在下次GC时就会被回收。
class Obj {
    pulic static Object strongReference = new Object();
}
复制代码

2.2 软引用(SoftReference)

若是对象具备软引用,则

  1. 内存空间充足时,垃圾回收器不会回收
  2. 内存空间不足时,就会尝试回收这些对象。只要垃圾回收器没有回收它,该对象就能够被程序使用
// 强引用
    String strongReference = new String("abc");
    String str = new String("abc");
    // 软引用
    SoftReference<String> softReference = new SoftReference<String>(str);
复制代码

软引用能够和一个引用队列(ReferenceQueue)联合使用。若是软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中

ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
    // 强引用
    String str = new String("abc");
    SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
	// 消除强引用
    str = null;
    // Notify GC
    System.gc();

    System.out.println(softReference.get()); // abc

    Reference<? extends String> reference = referenceQueue.poll();
    System.out.println(reference); //null
复制代码

注意:

  1. 软引用对象是在JVM内存不够的时候才会被回收,咱们调用System.gc()方法只是起通知做用,JVM何时扫描回收对象是JVM本身的状态决定的。
  2. 就算扫描到软引用对象真正开始GC也不必定会回收它,只有内存不够的时候才会回收。
  3. 软引用对象回收规则适应的前提条件是这个对象只有软引用。因此在上面的用例中要把强引用清除。

也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError以前回收软引用对象,并且虚拟机会尽量优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的较新的软对象会被虚拟机尽量保留

应用场景:

浏览器的后退按钮。按后退时,这个后退时显示的网页内容是从新进行请求仍是从缓存中取出呢?这就要看具体的实现策略了。

  1. 若是一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,须要从新构建;
  2. 若是将浏览过的网页存储到内存中会形成内存的大量浪费,甚至会形成内存溢出。

这时候就可使用软引用,很好的解决了实际的问题:

// 获取浏览器对象进行浏览
   Browser browser = new Browser();
   // 从后台程序加载浏览页面
   BrowserPage page = browser.getPage();
   // 将浏览完毕的页面置为软引用
   SoftReference softReference = new SoftReference(page);
   // 消除强引用
   page = null;
   
   // 回退或者再次浏览此页面时
   if(softReference.get() != null) {
       // 内存充足,尚未被回收器回收,直接获取缓存
       page = softReference.get();
   } else {
       // 内存不足,软引用的对象已经回收
       page = browser.getPage();
       // 从新构建软引用
       softReference = new SoftReference(page);
   }
复制代码

2.3 弱引用(WeakReference)

相比较软引用,具备弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它锁管辖的内存区域的过程当中,一旦发现了具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。不过,因为垃圾回收器是一个优先级很低的线程,所以不必定会很快发现那些只具备弱引用的对象。

String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    // 消除强引用
    str = null;
复制代码

一样,弱引用能够和一个引用队列(ReferenceQueue)联合使用,若是弱引用的对象被垃圾回收,JVM就会把这个弱引用加入到与之关联的引用队列中

ReferenceQueue<String> queue = new ReferenceQueue<>();
        String str = new String("abc");
        WeakReference<String> weakReference = new WeakReference<>(str, queue);
        str = null;
        System.gc();
        try {
            // 休息几分钟,等待上面的垃圾回收线程运行完成
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(weakReference.get());  // null
        System.out.println(queue.poll());  // java.lang.ref.WeakReference@22a71081
复制代码

2.4 虚引用(PhantomReference)

虚引用顾名思义,就是形同虚设。与其余几种引用都不一样,虚引用并不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收器回收。

应用场景:

虚引用主要用来跟踪对象被垃圾回收器回收的活动。 虚引用软引用弱引用的一个区别在于:

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,若是发现它还有虚引用,就会在回收对象的内存以前,把这个虚引用加入到与之关联的引用队列中。

String str = new String("abc");
    ReferenceQueue queue = new ReferenceQueue();
    // 建立虚引用,要求必须与一个引用队列关联
    PhantomReference pr = new PhantomReference(str, queue);
复制代码

程序能够经过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。若是程序发现某个虚引用已经被加入到引用队列,那么就能够在所引用的对象的内存被回收以前采起必要的行动。

3. 垃圾收集算法

3.1 标记-清除算法

标记-清除算法分为“标记”和“清除”两个阶段,执行过程以下图所示:

  1. 标记:首先标记出全部须要回收的对象
  2. 清除:在标记完成后统一回收全部被标记的对象

image.png

标记-清除算法主要有两个不足:

  1. 效率问题,标记和清除的两个过程效率都不高
  2. 标记-清除会产生大量不连续的内存碎片,这会致使在后面须要分配连续的大对象时,没法找到足够大的连续内存而致使不得不提早触发另外一次垃圾收集动做

3.2 复制算法

复制算法的大体思路以下,其执行过程以下图所示:

  1. 首先将可用内存分为大小相等的两块,每次只使用其中的一块。
  2. 当这一块的内存用完了,就将还存活的对象连续复制到另外一块上面,而后把使用过的内存空间一次清理掉

复制算法的代价就是将内存缩小为原来的一半。

image.png

如今的商业虚拟机都是采用复制算法来回收新生代。

  1. 新生代的内存分为一块较大的Eden空间和两块较小的Survivor空间。
  2. 每次使用Eden和一块Survivor,当进行回收是,将Eden和Survivor中还存活的对象一次性复制到另外一个Survivor空间上。而后,清理掉Eden和刚刚使用过的Survivor空间。
  3. HotSpot虚拟机默认Eden和Survivor的大小比例为8 : 1,这样每次新生代可用内存为整个新生代的90% (10% + 80%),只有10%的内存会被浪费。

3.3 标记-整理算法

标记-整理算法分为“标记”和“整理”两个阶段,执行过程以下图所示:

  1. 标记:首先标记出全部须要回收的对象
  2. 整理:让全部的存活的对象都向一端移动,而后直接清除掉边界之外的内存

image.png

3.4 分代收集算法

分代收集算法就是降Java堆分为新生代和老年代,根据其各自的特色采用最适当的收集算法。

  1. 新生代中大批对象死去,只有少许存活,就选用复制算法
  2. 老年代中对象存活概率高,没有额外的空间对它进行分配担保,就必须使用标记-清除或者标记-整理算法。

4. 垃圾回收器

JVM垃圾收集器发展历程大体能够分为如下四个阶段: Serial(串行)收集器 -> Parallel(并行)收集器 -> CMS(并发)收集器 -> G1(并发)收集器

下图展现了7种做用域不一样分代的收集器,若是两个收集器之间存在连续,就说明它们能够搭配使用。下面逐一介绍这些收集器的特性、基本原理和使用场景。

4.1 Serial类收集器

Serial类收集器是一个单线程的收集器:

  1. 它只会用单个收集线程去进行垃圾回收的工做

  2. 它在进行垃圾收集的时候会“Stop The World”暂停其余全部的工做表线程,直到它收集结束

  3. Serial收集器采起复制算法新生代进行单线程的回收工做

  4. Serial Old收集器采起标记-整理算法在老年代进行单线程的回收工做

image.png

4.2 Parallel类收集器

Parallel类收集器就是Serial收集器的多线程版本:

  1. 它使用多个收集线程取进行垃圾回收工做
  2. 它在进行垃圾收集的时候也会“Stop The World”暂停其余全部的工做表线程,直到它收集结束
  3. ParNew收集器采起复制算法新生代进行多线程的回收工做
  4. Parallel Scavenge收集器也是一个新生代收集器,不过它被称为“吞吐量优先”收集器,它提供了2个能精确控制吞吐量的参数:
    • -XX : MaxGCPauseMillis:控制最大垃圾收集停顿时间
    • -XX : GCTimeRatio : 直接设置吞吐量大小,垃圾收集时间占总时间的比率

Parallel Scavenge收集器还有一个开关参数-XX: UseAdaptiveSizePolicy,打开这个开关后就不用手动指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio)等细节参数了,JVM会动态调整这些参数已提供最合适的停顿时间或者最大吞吐量。

  1. Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法在老年代进行垃圾回收。

image.png

4.3 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它是一个基于标记-清除算法实现的,运做过程分为4个步骤:

  • 初始标记(CMS initial mark): 须要“Stop The World”,仅仅只是标记下GC Roots能直接关联到的对象,速度很快

  • 并发标记(CMS concurrent mark): CMS线程与应用线程一块儿并发执行,从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长

  • 从新标记(CMS remark):从新标记就是为了修正并发标记期间因用户线程继续运做而致使标记产生变更的那一部分对象的标记记录,能够多线程并行

  • 并发清除(CMS concurrent sweep):CMS线程与应用线程一块儿并发执行,进行垃圾清除

    image.png

    CMS收集器优势:并发收集低停顿

    CMS的三个明显的缺点:

    1. CMS收集器对CPU的资源很是敏感。CPU的数量较少不足4个(好比2个)时,CMS对用户程序的影响就可能变的很大。
    2. CMS收集器没法处理浮动垃圾(Floating Carbage),可能出现"Concurrent Mode Failture"失败而致使产生另外一次Full GC的产生。浮动垃圾就是并发清理阶段,用户线程产生的新垃圾
    3. CMS是基于标记-清除算法的,收集结束后会有大量的空间碎片,就可能会在老年代中没法分配足够大的连续空间而不得不触发另外一次Full GC。

4.4 G1收集器

同优秀的CMS同样,G1也是关注最小停顿时间的垃圾回收器,也一样适合大尺寸堆内存,官方也推荐用G1来代替选择CMS。

  1. G1收集器的最大特色就是引入了分区的思路,弱化了分代的概念
  2. G1从总体来看是基于标记-整理算法实现的,从局部(两个Region之间)来看是基于复制算法实现的

4.4.1 G1相对于CMS的改进

  1. G1是基于标记-整理算法,不会产生空间碎片,在分配大的连续对象是不会由于没法获得连续空间而不得不提早触发一次Full GC
  2. 停顿时间可控,G1能够经过设置停顿时间来控制垃圾回收时间
  3. 并行与并发,G1能更充分的利用CPU,多核环境下的硬件优点来缩短stop the world的停顿时间

4.4.2 G1与CMS的区别

(1)堆内存模型的不一样

G1以前的JVM堆内存模型,堆被分为新生代,老年代,永久代(1.8以前,1.8以后是元空间),新生代中又分为Eden和两个Survivor区。

img

  1. G1收集器的堆内存模型,堆被分为不少个大小连续的区域(Region),Region的大小能够经过-XX: G1HeapRegionSize参数指定,大小区间为[1M,32M]。

  2. 每一个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor,老年代和巨型区(Humongous Region)。巨型区域是为了存储超过50%标准region大小的巨型对象。

    img

(2)应用分代的不一样

G1能够在新生代和老年代使用,而CMS只能在老年代使用。

(3)收集算法的不一样

G1是复制+标记-整理算法,CMS是标记清除算法。

4.4.3 G1收集器的工做流程

G1收集器的工做流程大体分为以下几个步骤:

  1. 初始标记(Initial Marking): 须要“Stop The World”,仅仅只是标记下GC Roots能直接关联到的对象,速度很快
  2. 并发标记(Concurrent Marking): G1线程与应用线程一块儿并发执行,从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长
  3. 最终标记(Final Marking): 最终标记阶段则是为了修正在并发标记期间因用户程序继续运做而致使标记产生变更的那一部分标记记录,须要“Stop The World”,能够多线程并行
  4. 筛选回收(Live Data Counting and Evacuation): 对各个Region的回收价值和成本进行排序,根据用户所期待的GC停顿时间制定回收计划。具体地,在后台维护一个优先队列,每次根据容许的收集停顿时间,优先回收价值最大的Region

4.4.4 G1的GC模式

G1提供了两种GC模式,Young GC和Mixed GC,两种都是彻底Stop The World的

(1)YoungGC
  1. 在分配通常对象(非巨型对象)时,当全部的Eden Region使用达到最大阈值而且没法申请到足够内存时,会触发一次YoungGC。
  2. 每次YoungGC会回收全部的Eden以及Survivor区,而且将存活对象复制到Old区以及另外一部分的Survivor。
(2)MixedGC

当愈来愈多的对象晋升到老年代old region时,为了不堆内存被耗尽,虚拟机会触发一次mixed gc,该算法并非一个old gc,除了回收整个young region,还会回收一部分的old region。这里须要注意:是一部分老年代,而不是所有老年代,能够选择哪些old region进行收集,从而能够对垃圾回收的耗时时间进行控制

G1没有fullGC概念,须要fullGC时,调用serialOldGC进行全堆扫描(包括eden、survivor、o、perm)。

参考与感谢

  1. 理解Java的强引用、软引用、弱引用和虚引用
  2. 深刻剖析JVM:G1收集器+回收流程+推荐用例
  3. Java Hotspot G1 GC的一些关键技术
相关文章
相关标签/搜索