什么是垃圾

在进行垃圾回收之前,就要先判断出什么是垃圾。

垃圾其实指的是在这次运行程序中没有被任何指针指向的对象,也就是没有被引用机会的对象,因为没有指向也就无法引用,所以这些对象就是要清理的垃圾。

如果垃圾不及时的清理,那么在这次运行程序的过程中就会一直占用部分内存,严重的还会导致内存溢出

内存溢出就是指程序运行过程中申请的内存大于系统所能够提供的内存,这就会导致无法申请足够的内存,于是就发生了内存溢出。

哪里需要垃圾回收

垃圾回收就需要关注哪里产生垃圾,其实垃圾的产生主要在运行时数据区中的方法区。因为栈是用完就会推出的,计数器也不需要回收,所以GC的作用区域就是方法区和堆了。虽然说这两块区域会有GC行为,但正常来说主要的还是堆空间的GC,方法区很少会有GC行为。

垃圾回收的算法

垃圾标记阶段

垃圾标记阶段就是为一些死的或者活的对象打上标记,用来区分哪些是垃圾,哪些是用的上的对象。判断对象存活的方法一般有两个,引用计数算法和可达性分析算法。

引用计数算法

计数算法其实就是为每个对象保存以个整型的引用计数器属性,每当该对象被引用了,改对象的引用计数器就会加1,引用失效了计数器就会减1,只要对象的计数器为0,即表示该对象已经不会被引用了,就会为它做上一个标记。

引用计数算法是很简单高效的算法,但是对象一多,每个对象的计数器也是一个很大的开销。
而且引用计数算法还有一个很严重的问题,无法处理循环引用的情况,这是很致命的,所以Java的垃圾回收器没有使用这个算法。

循环引用问题

从上图就可以看出对象的引用关系只要形成环形,很大的可能就会出现循环引用的问题,这就会出现内存泄漏的问题。

分析Java是否使用引用计数算法

  1. 配置JVM的虚拟机参数,打印JVM的情况。
    我是使用IDEA,所以按照IDEA的配置来讲。点击RUN->Edit Configurations->Applacion->VM options。配置以下参数

    1
    -XX:+PrintGCDetails
  2. 模拟循环引用的情况。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.yw;
    /**
    * @ClassName ReferenceCount
    * @Descriprtion TODO
    * @Author yww
    **/
    public class ReferenceCount {
    // 设置引用属性
    Object reference = null;
    public static void main(String[] args) {
    ReferenceCount A = new ReferenceCount();
    ReferenceCount B = new ReferenceCount();
    // 形成引用环
    A.reference = B;
    B.reference = A;
    // 断开外部引用,A与B形成循环引用
    A = null;
    B = null;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 没有显示调用GC的内存情况
    Heap
    PSYoungGen total 38400K, used 3330K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)
    eden space 33280K, 10% used [0x00000000d5f80000,0x00000000d62c09a8,0x00000000d8000000)
    from space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)
    to space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)
    ParOldGen total 87552K, used 0K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)
    object space 87552K, 0% used [0x0000000081e00000,0x0000000081e00000,0x0000000087380000)
    Metaspace used 3141K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 342K, capacity 388K, committed 512K, reserved 1048576K
  3. 显示调用的GC之后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.yw;
    /**
    * @ClassName ReferenceCount
    * @Descriprtion TODO
    * @Author yww
    **/
    public class ReferenceCount {
    // 设置引用属性
    Object reference = null;
    public static void main(String[] args) {
    ReferenceCount A = new ReferenceCount();
    ReferenceCount B = new ReferenceCount();
    // 形成引用环
    A.reference = B;
    B.reference = A;
    // 断开外部引用,A与B形成循环引用
    A = null;
    B = null;
    // 显示调用GC
    System.gc();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //	使用了GC之后的内存情况
    [GC (System.gc()) [PSYoungGen: 2664K->776K(38400K)] 2664K->784K(125952K), 0.0012757 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [Full GC (System.gc()) [PSYoungGen: 776K->0K(38400K)] [ParOldGen: 8K->617K(87552K)] 784K->617K(125952K), [Metaspace: 3136K->3136K(1056768K)], 0.0039173 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    Heap
    PSYoungGen total 38400K, used 998K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)
    eden space 33280K, 3% used [0x00000000d5f80000,0x00000000d6079b20,0x00000000d8000000)
    from space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)
    to space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)
    ParOldGen total 87552K, used 617K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)
    object space 87552K, 0% used [0x0000000081e00000,0x0000000081e9a458,0x0000000087380000)
    Metaspace used 3148K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 343K, capacity 388K, committed 512K, reserved 1048576K
  4. 分析
    可以清楚的看到调用了GC之后,内存的占用率变低了,如果Java使用的是引用计数算法,那么内存是不应该降低的,所以可以知道的是Java不适用引用计算这种算法。

可达性分析算法

可达性分析算法又称根搜索算法,追踪性垃圾收集。该算法可以有效的解决计数算法中的循环引用的问题,防止内存泄漏的发生。

GC Roots

GC Roots根集合,是一组必须活跃的引用。可达性分析算法的思路就是以GC Roots为起始点,从上至下的搜索被根对象引用的对象,查看对象是否可达。

搜索的路径被称为引用链。

存活的对象都应该是直接或者被间接引用到的,即都是可达的。

GC Roots可以是哪些

  1. 虚拟机栈中引用的对象
  2. 本地方法栈内引用的对象
  3. 方法区中静态属性引用的对象
  4. 方法区常量引用的对象
  5. Java虚拟机内部的引用
  6. 所有被同步锁synchronized持有的对象
  7. 除了固定的GC Roots集合外,根据用户选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时加入,比如分代收集和局部回收。

对象的finalization机制

finalization机制称为对象终止机制,这个机制允许开发人员在对象销毁之前可以自定义逻辑代码。

即垃圾回收之前,总会先调用这个对象的finalize方法,这个方法可以被子类重写,通常用于在对象被回收时进行资源释放。

由于finalize方法的存在,虚拟机的对象一般处于三种可能的状态。

  1. 可触及的,即可达的。
  2. 可复活的。不可触及的对象有可能会在finalize方法中复活。
  3. 不可触及的。因为finalize方法只调用一次,当finalize方法被调用对象也没有复活,那就是不可触及的了。

于是虚拟机判断一个对象是否可回收就会有这样的过程。

  • 对象不在GC Roots的引用链当中,就会被第一次标记。
  • 对象没有重写finalize方法,或者finalize方法已被调用,则被判定为不可触及的。
  • 对象重写了finalize方法,且没被执行,那该对象就会进入Finalizer线程触发其finalize方法,然后进行第二次标记。
  • 要是该对象的finalize方法进入了引用链,即复活了,就会被移除回收的集合,finalize方法只能执行一次,之后再进入回收的集合就会直接变成不可触及。

清除阶段

当垃圾被标记之后,就要开始进行清除阶段了。

标记-清除算法

当堆的有效内存被耗尽的时候,就会停止整个程序,进行标记和清除这两个工作。

  • 标记:Collector从引用根节点遍历,标记所有被引用的对象,即在对象的Header中标记为可达的对象。
  • 清除:Collector对堆内存进行全部遍历,发现没有标记的就会将其回收。

这种方法效率不高,而且进行GC的时候会停止整个程序,清理垃圾后也会产生内存碎片。

内存碎片表示在内存中散乱的存放着一些对象。当有新的对象需要被加载的时候,可能就会没有足够的连续的内存空间给新的对象,所以即使总的空闲空间是足够的,也不能没有空间存放这个新的对象。

这里的清除不是表示直接删除该对象,其实这些对象还是存放在内存中,只是会将这部分对象所在的内存加入到空闲列表中,加载新的对象时候,就会将其覆盖掉。

复制算法

将活着的内存空间分为两块(设两块区域为A和B),每次只用一块(设第一次使用的是A),垃圾回收的时候将A中存活的对象复制到B区域当中,然后清除A中所有的对象。当下次B要进行垃圾回收了就复制到A,清除B中所有对象。交换使用。

这种方法复制到新区域的对象就会是连续的内存空间,就不会产生内存碎片的问题,但缺点也很明显,减少了总体的内存空间,因为分成两半了,而且当需要复制的存活对象很多时候,就会占用很多内存和浪费很多时间。

标记-整理(压缩)算法

  • 标记:Collector从引用根节点遍历,标记所有被引用的对象,即在对象的Header中标记为可达的对象。
  • 整理:标记完了之后就会将所有存活的对象整理在内存中的位置,使其连续排放。

标记-整理算法其实就像是在标记-清除算法后进行一次内存碎片的整理,只是少了清除的遍历,不过因为内存地址变了,有些引用的地址也会变,所以会存在一些风险。

清除阶段三种算法的对比

Mark-SweepCopyingMark-Compect
速度中等最快最慢
空间开销少,但是会有内存碎片大,需要两倍的空间少,没有内存碎片
是否要移动对象

分代收集算法

  • 年轻代
    年轻代的对象生命周期短,回收频繁,所以使用复制算法是比较好的,复制算法的内存问题可以通过hotspot中的两个survivor的设计得到缓解。
  • 老年代
    老年代的对象生命周期长,所以一般是使用标记-清除或者是标记-清除和标记-整理混合使用。
  • 几乎所有的垃圾回收器都会区分新生代和老年代。

增量收集算法

如果等到内存空间不够然后一次性的进行垃圾回收,就会出现程序停止的问题,所以增量收集算法就是让垃圾收集的进程和应用程序的进程交替使用,每次只收集一片区域的内存空间。

分区算法

分区算法和增量收集算法其实差不多,分区算法就是将堆空间分割成多个小块的内存,每次合理回收若干个小的区间,从而减少一次CG产生的停顿时间。

垃圾回收的相关知识

System.gc方法

  • 使用System.gc()或者是Runtime.getRuntime().gc()就可以显示触发Full GC,同时回收老年代和新生代,尝试释放被丢弃对象占用的内存。
  • 调用该方法会附带一个免责声明,即会触发Full GC,但是不知道会什么时候触发。

内存溢出(OOM)

  • 内存溢出其实不太容易出现,如果当应用程序占用的内存增长速度异常,超过了垃圾回收提供的空闲内存,那就会可能出现内存溢出。
  • 内存溢出之前其实都会进行一次独占式的Full GC,回收大量内存,若果此时内存还是不够用,就会出现内存溢出的情况。
  • Javadoc对OOM的理解是,没有空闲内存并且垃圾收集器也无法提供更多内存,就会报OOM。

出现的原因可能有以下原因:

  1. Java虚拟机的堆内存设置不够
  2. 代码创建大量对象,而且长时间不能被垃圾收集器收集。

内存泄漏(Memory Leak)

  • 严格来讲,对象不会被程序用到了,但是GC又回收不掉这些对象的情况,叫内存泄漏。
  • 宽松一点来讲,一些不当操作导致对象的生命周期变得很长甚至导致OOM,也可以叫做内存泄漏。
  • 内存泄漏不会立刻引起程序崩溃,但是发生内存泄漏之后,就有可能不断泄漏,然后就可能会出现OOM,即内存溢出的情况,然后导致程序崩溃。
  • 注意,循环引用是针对引用计数算法的内存泄漏,Java不使用引用计数算法,所以这种不算是内存泄漏。

一些内存泄漏的例子:

  1. 单例模式
    单例的生命周期和应用程序一样长,所以当单例引用了外部对象时候,这个外部对象即一直是可达的了,这就会导致内存泄漏的产生。
  2. 一些提供close方法的对象未关闭导致的内存泄漏
    比如说数据库连接,网络连接和IO的连接操作,需要手动关闭,不然就会出现内存泄漏。

Stop The World

Stop The World简称STW,指GC事件发生的过程中会产生应用程序的停顿,停顿时整个应用程序线程都会被暂停,这个停顿就是STW。

比如可达性分析算法。可达性分析算法中的GC Roots的是可能会随着程序的而不断更新的,所以当进行枚举GC Roots的时候,会导致Java执行线程停顿。

所有的GC都会有STW的情况发生,不能避免,只能尽量减少STW的时间

安全点

事实上,发生GC的时间点并不是随机的,意思就是GC伴随的STW的不是随机某个时间点发生的,只有某一些特殊的位置才会停下来GC,这些位置就称作安全点。但是如何在GC发生的时候,检查到所有的线程都跑到最近的安全点然后开始停顿呢?

  • 抢先式中断(目前没有虚拟机采用了)
    中断所有线程,如果还有线程不在安全点就恢复该线程使其运行到最近的安全点。
  • 主动式中断
    设置一个中断标志,各个线程运行到安全点的时候主动轮询这个标志,如果中断标志为真,就将该线程中断挂起。

安全区域

讲述安全点的时候,默认把所有进程认为都在运行着,可是如果有某个进程处于睡眠或者阻塞的时候,还没到安全点就挂着了,这种时候还会一直等到该进程进入安全点吗?这种时候就需要设置安全区域来解决了。

安全区域是指在一段片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。这其实很好理解,对象的引用关系不发生变化的时候,GC Roots是不会发生变化的,可达性分析算法也就不会受到影响,所以在这段区域中停顿是不会对GC产生影响的。安全区域其实也意味着,这段区域都是安全点。

所以线程执行的时候要发生GC的就会是这样的:

  1. 当线程进入安全区域的时候,就会标识该线程进入安全区域,如果要发生GC了,就会忽略该线程的情况,该发生就发生,该等就等。
  2. 当线程离开安全区域的时候,会检查JVM的GC是否已经完成,完成了该线程就继续运行,否则线程就会等待可以离开安全区域的信号为止。

引用

引用分为四种,按照引用类型的强度递减为强引用,软引用,弱引用和虚引用。

强引用(StrongReference)

强引用是最经常用到的,平时的引用关系的建立很多都是强引用类型的,就比如Object obj = new Object()这种引用关系的建立就是强引用。强引用的关系存在的时候,就会被可达性分析算法标识为可触及的,所以就不会被垃圾收集器回收。

  • 强引用-不回收,即使要发生OOM了也不会进行回收。
  • 建立了强引用可以直接访问目标对象。
  • 强引用是导致内存泄漏的一个主要原因。

软引用(SoftReference)

软引用就是弱一点的强引用。在系统即将发生OOM的时候,垃圾回收器就会进行第二次回收(第一次是那些不可达的对象),把这些被软引用关系的对象清除掉,要是清除掉之后还是没有足够内存,才会发生OOM。

  • 软引用-内存不足即回收。用来使用在一些虽然有用,但是不是必需的对象上。

  • 软引用通常实现缓存方面上

  • 建立软引用可以通过使用SoftReference的类来实现。

    1
    2
    3
    Object obj = new Object();
    SoftReference<Object> sr = new SoftReference<>(obj);
    obj = null; // 销毁强引用

    弱引用(WeakReference)

弱引用的关系在建立了之后就只能活到下一次的垃圾回收,因为GC发生的时候,即使内存足够也会把弱引用的对象全部回收掉。

  • 弱引用-发现即回收。

  • GC回收的时候,会通过算法检查软引用是否要回收,而弱引用对象被检查到会直接被回收。

  • 弱引用也是主要用在缓存方面

  • 建立弱引用可以通过使用WeakReference的类来实现。

    1
    2
    3
    Object obj = new Object();
    WeakReference<Object> wr = new WeakReference<>(obj);
    obj = null; // 销毁强引用

    虚引用(PhantomReference)

虚引用关系是否存在对对象的生存不构成影响,也无法通过虚引用来实例化一个对象,该引用的目的只是为了在被虚引用的对象要被垃圾回收的时候会收到一个系统通知。

  • 虚引用-对象回收跟踪。

  • 虚引用相当于没有引用,也获取不到对象。

  • 虚引用必须和引用队列搭配使用,因为当对象被发现是虚引用的时候,就会在垃圾回收之后,将该虚引用加入到引用队列当中,用来通知程序。

  • 建立虚引用可以通过使用PhantomReference的类来实现。

    1
    2
    3
    4
    Object obj = new Object();
    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference<Object> pr = new PhantomReference<>(obj,queue);
    obj = null; // 销毁强引用

    垃圾回收器

分类

  • 垃圾回收器可以由不同厂商,不同版本的JVM来实现。
  • 按照线程数来分类,可以分为串行垃圾回收器并行垃圾回收器
  • 按照工作模式来分类,可以分为并发式垃圾回收器独占式垃圾回收器
  • 按照碎片处理方式来分类,可以分为压缩式垃圾回收器非压缩式垃圾回收器
  • 按照工作的内存区间来分类,可以分为年轻代垃圾回收器老年代垃圾回收器

性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例。
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用的时间占总运行时间的比例。
  • 暂停时间:执行垃圾回收的时候,工作线程被暂停的时间。
  • 收集频率。收集操作发生的频率。
  • 内存占用:Java堆区所占用的内存大小。
  • 快速:一个对象从诞生到被回收的时间。

七款经典的收集器

  • 新生代收集器: Serial,ParNew,Parallel,Scavenge
  • 老年代收集器:Serial Old,Parallel Old,CMS
  • 整堆收集器:G1
  • JDK8中默认使用的是ParallelParallel Old的组合,JDK9中默认使用G1

1. 红色的虚线表示的这两个组合在JDK8已起用,JDK9已移除
2. 绿色的虚线表示的这个组合在JDK14中已经弃用
3. CMS GC的虚线表示在JDK14中已经删掉了这个垃圾回收器。

Serial GC和Serial Old GC

  • Serial收集器是属于串行的垃圾回收器

  • Serial收集器采用复制算法串行回收Stop The World机制的方式执行内存回收

  • Serial收集器是收集年轻代的,也提供了收集老年代的收集器,Serial Old

  • Serial Old收集器采用标记-压缩算法串行回收Stop The World机制的方式执行内存回收

  • Serial Old是运行在Client模式下默认的老年代垃圾回收器

  • Serial Old在Server模式下有两个用途

    1. 与新生代的Parallel Scavenge配合使用
    2. 作为老年代CMS收集器的后备垃圾收集方案
  • 可以设置参数使用Serial收集器,老年代也会默认使用Serial Old

    1
    -XX:+UseSerialGC

    现在CPU发展很快,串行的收集器体验不好,特别是对现在高流量的信息时代,所以一般Java Web中不会采用串行垃圾收集器。

ParNew GC

  • ParNew 是一款并行的垃圾收集器,可以说是Serial GC的多线程版本

  • ParNew GC只处理新生代

  • ParNew GC采用了复制算法Stop The World机制,相对Serial GC来说只是暂停时间更短而已

  • 很多的运行在Server模式下的JVM会采用ParNew GC为默认垃圾回收器

  • 不考虑版本的话,ParNew GC可以搭配CMS GC或者是Serial GC使用

  • 对于新生代回收次数频繁,所以使用ParNew GC并行可以提高效率,老年代回收少,使用串行垃圾回收器可以节省资源

  • 设置参数使用ParNew GC

    1
    -XX:+UseParNewGC

    前面有提到一点就是在JDK9版本中,Serial Old GC不可以与ParNewGC搭配了,在之后CMS也会删除,所以ParNew GC也快到了被移除的阶段了,所以不怎么建议使用了。

Parallel Scavenge GC 和 Parallel Old GC

  • Parallel与ParNew都是并行的,两者性能相差不大

  • Parallel Scavenge GC 采用复制算法Stop The World机制

  • Parallel Scavenge GC 与ParNew GC不同的是Parallel收集器目标是达到一个可控制的吞吐量,也被称为吞吐量优先的垃圾回收器,自适应调节策略两者也有很大不同。

  • 因为Parallel Scavenge GC是以吞吐量为优先的,所以老年代的收集器就不适合用串行的了,所以在JDK1.6的时候提供了Parallel Old GC用来替换Serial Old GC

  • Parallel Old GC采用标记-整理算法,同样基于并行回收Stop The World

  • 参数设置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 默认开启一个,另一个也会开启
    -XX:+UseParallelGC
    -XX:+UseParallelOldGC
    // 限制并行的线程,默认与CPU核心数量相等
    -XX:ParallelGCThreads
    // 设置STW最大时间
    -XX:MaxGCPauseMillis
    // 设置Parallel Scavenge GC开启自适应调节策略,默认开启
    -XX:+UseAdaptiveSizePolicy

    CMS GC

详情可以查看这篇文章