1、垃圾回收概述

JVM可以自动进行垃圾回收,无需程序员自己耗费精力进行垃圾回收。那么对JVM来说,什么是垃圾:
垃圾是指运行程序中,没有任何指针指向的对象。这种对象就是需要被回收的垃圾,
垃圾回收,我们需要知道:
哪些内存需要回收:
引用计数法、可达性分析法;
什么时候回收:
安全点、安全区域;
如何回收:
标记-清除、标记-复制、标记-整理、分代收集等相关算法,以及各种垃圾收集器。

2、垃圾回收相关算法

2.1 标记阶段:判断哪些对象是垃圾的算法

有两种算法,引用计数法和可达性分析法。Java使用的是可达性分析法。

引用计数法:

  • 对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况;
  • 对象被引用了就+1,引用失效就-1,为0时就表示不可能再被使用,可以回收;
  • 优点:实现简单,垃圾便于识别,判断效率高,回收没有延迟性;
  • 缺点:

    • 需要单独的字段存储计数器,增加了存储空间的开销;
    • 每次赋值需要更新计算器,伴随着加减法的操作,增加了时间开销;
    • 致命缺陷:无法处理循环引用的情况。

      可达性分析法:

      基本思路:
  • 以根对象(GC Roots)为起始点,按照从上到下的方式搜索被根对象集合所连接到的目标对象是否可达;

  • 使用可达性分析算法后,内存中存活的对象都被跟对象集合直接或者间接连接着,搜索所走过的路径称为引用链;
  • 如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡,就可以标记为垃圾对象;
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活的对象

哪些对象可以作为GC Roots:

  • 虚拟机栈中引用的对象:比如各个线程被调用的方法中使用到的参数、局部变量;
  • 方法区中静态属性引用的对象:比如:java类的引用类型静态变量;
  • 方法区中常量引用的对象:比如字符串常量池里的引用;
  • 所有被同步锁synchronized持有的对象;
  • 本地方法栈内JNI,引用的对象;
  • Java虚拟机内部的引用;
  • 反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等;
  • 除了固定的GC Roots集合之外,根据用户选择的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性的加入,共同构成完整GCRoots集合,比如分代收集和局部回收。

2、清除阶段:回收垃圾相关的算法

标记-清除算法:

标记清除算法分为标记和清除两个阶段。
标记阶段:从引用根节点(GC Roots)开始遍历,标记所有被引用的对象,一般是在对象的Header中记录为可达对象(注意标记的是有引用的对象,而不是垃圾对象);
清除阶段:从堆内存从头到尾进行线性的遍历,如果发现某个对象,其Header中没有被标记为可达对象,则将其回收。
优点:它不需要进行对象的移动,并且仅仅对垃圾对象进行处理,所以在存活对象较多的情况下极为高效。
缺点:在GC的时候需要停止整个应用程序,用户体验差;这种方式直接清理垃圾对象,会导致清理出的空间内存不可连续,产生内存碎片,需要维护一个空闲列表。
注:这里的清除,并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够用,如果够,就存放。

复制算法:

复制算法,是把内存空间分为两块,每次只使用其中一块。在垃圾回收时,将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
这种算法是为了客服句柄的开销和解决堆碎片问题。建立在存活对象少,垃圾对象多的前提下。
优点:没有标记和清除的过程,实现简单高效;同时复制后的内存空间保证了连续性,不会出现内存碎片的问题;
缺点:需要两倍的内存空间;对于G1这种拆分为大量region(区域)的垃圾收集器,复制而不是移动,意味着垃圾收集器需要维护region之间的引用关系,不管是内存占用或者时间开销,都比较大;需要系统中垃圾对象很多,需要复制的存活对象数量不多的情况。

标记-整理算法:

也叫标记-压缩算法:

  • 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用的对象;
  • 第二阶段将所有的存活对象压缩(或者说)整理在内存的一端,按照顺序排放;
  • 之后清理边界外所有的空间;
  • 最终效果等同于标记清除算法执行完成后,再进行一次内存碎片整理。
  • 与标记清除算法的本质区别:标记清除算法是非移动式的算法,标记压缩是移动式的。
  • 优点是消除了标记清除内存区域分散的缺点;没有复制算法中,内存减半的代价;
  • 缺点是:从效率上,标记整理算法效率低于复制算法;移动对象的同时,如果对象被其他对象引用,需要调整引用的地址;移动到过程中,需要暂停用户应用程序,即STW。


分代收集算法:

  • 不同生命周期的对象可以采取不同额收集方式,以便提高回收效率;
  • 几乎所有的GC收集器都采用分代收集算法执行垃圾回收的;
  • 次数上频繁收集新生代,较少收集老年代,几乎不动永久代(元空间);
  • 在HotSpot中,新生代的对象生命周期短,存活率低,回收频繁;老年代内存区域较大,对象生命周期长,存活率高,回收不如新生代频繁。

增量收集算法:

增量收集算法思想:
每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成;
通过对线程间冲突的妥善管理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作;
缺点是:线程和上下文切换导致系统吞吐量下降;

分区算法:

为了控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的时间;
分代算法是将对象按照生命周期长短划分为两个部分,分区算法是将整个堆划分为连续的不同的小区间;
每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个小区间;

3、 垃圾回收相关概念:

System.gc()的理解:
System.gc或Runtime.getRuntime().gc()的调用,会显示触发FullGC,同时会对老年代和新生代进行回收,尝试释放被丢对象占用的内存;
System.gc调用无法保证对垃圾收集器的调用;
一些特殊情况下,比如编写性能基准,我们可以在运行之间调用System.gc;
内存泄露和内存溢出:
内存泄漏:
只有对象不再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露;
实际情况有一些疏忽导致对象的生命周期变的很长甚至OOM,宽泛意义上的内存泄露;
举例:单例的生命周期和程序是一样长,如果单例程序中,持有对外部对象的引用的话,那么这个外部对象是不能被回收的,导致内存泄露;一些提供close的资源未关闭导致内存泄露,如数据库链接,网络链接,和IO。
内存溢出:
即OOM,Out Of Memory。堆内存中出现OOM错误的原因有两种:一个是JVM的堆内存设置的不够用;另一个是代码创建大量对象,或者说大量大对象,并且长时间不能被垃圾收集器收集。

Stop The World:
即STW。是指会停止应用程序线程。
通常在垃圾收集时会发生,当然还有其他情况也会发生STW,这个后续再学习。

垃圾回收的并行与并发:
并发:同一时间段内,几个程序都在同一个处理器上运行;CPU切换;
并行:一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互相不抢占资源,可以同时进行,我们称之为并行;并行因素取决于CPU的核心数量;
并发时多个任务之间抢占资源,并行多个任务之间不互相抢占资源;
垃圾回收的并发与并行:
并行:多条垃圾收集器并行工作,用户线程处于等待状;
串行:单线程执行。

安全点与安全区域:
安全点:
程序执行时,并不是在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来进行GC,这些位置称为安全点;
安全点如果太少,会导致GC等待时间长,如果太多,会导致运行时的性能问题。
大部分指令执行都比较短,通常会根据是否具有让程序长时间执行的特征为标准选择一些执行时间较长的指令作为安全点,比如方法调用,循环跳转和异常跳转等。
抢先式中断:中断所有线程,如果还有线程不在安全点,就恢复线程,让线程跑到安全点。但是没有虚拟机采用这种方式。
主动式中断:设置一个中断标志,各个线程运行到安全点的时候,主动轮询这个标志,如果标志为真,则将自己进行中断挂起。
安全区域:
如果线程处于sleep或者blocked状态,这时候线程无法响应jvm中断请求,走到安全点去中断挂起。对于这种情况,就需要安全区域来解决;
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何位置开始GC都是安全的;
当线程运行到安全区域代码时,首先标志已经进入了安全区域,如果GC,JVM会忽略标识为安全区域状态的线程;
当线程即将离开安全区域时,会检查JVM是否已经完成GC,如果完成了,则继续运行。否则线程必须等待直到收到可以安全离开安全区域的信号为止。
强、软、弱、虚 四种引用:
强引用:
垃圾回收 - 图1

  • 最传统和用的最多的引用定义,new一个对象就是强引用。无论任何情况,只要存在强引用,垃圾收集器永远不会回收掉被引用的对象。
  • 强引用是造成Java内存泄露的主要原因之一。
  • 强引用可以直接访问目标对象。

软引用:

  • 软引用通常用来实现内存敏感的缓存,高速缓存就有用到软引用;
  • 系统将要发生内存溢出之前,会将这些对象列入回收范围之中进行第二次回收,如果这些回收后还没有足够内存,才会抛出内存溢出异常;
  • 垃圾回收器在某个时间决定回收软可达的对象的时候,会清理软引用,并可选的把引用存放到一个引用队列;

弱引用:
只被弱引用关联的对象只能够生生存到下一次垃圾收集器之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象
虚引用:
一个对象是否有虚引用存在,完全不会对其生存时间构成影响。唯一目的就是在这个对象被收集器回收时收到一个系统通知;
他不能单独使用,也无法通过虚引用获取被引用的对象。

终结器引用:
用以实现对象的finalize方法,所以被称为终结器引用;
无需手动编码,其内部配合引用队列使用;
GC时,终结器引用入队,由finalize线程通过终结器引用找到被引用对象并调用 他的finalize方法,第二次GC时才能回收被引用对象;

4、垃圾回收器

4.1 垃圾回收器与性能指标

垃圾回收器的分类: