垃圾回收需要完成的三件事:
①哪些内存需要回收;
②什么时候回收;
③如何回收。

3.1 哪些内存需要回收

  • 方法区

3.2 什么时候回收

什么时候回收, 是需要判断对象是否存活, 不存活的即需要回收?
那么如何来判断对象是否活着?

a.判断对象是存活

  • 引用计数算法
    • 给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1,每当一个引用失效时,计数器减1,任何时刻计数器为0则代表对象不被引用。

注意:引用计数算法有一个比较大的问题,那就是它不能处理环形数据。即如果有两个对象相互引用,那么这两个对象计数器始终不为0,也就不能被回收。

  • 可达性分析算法

    • 设立若干根对象(GC Roots),每个对象都是一个子节点。从根向下搜索所走过的路径叫引用链,当一个对象到根无任何引用链相连,证明此对象不可用。
    • Java语言中可以作为GC Roots的对象包括以下几种:
      • 虚拟机栈中引用的对象
      • 方法区中类静态属性引用的对象
      • 方法区中常量引用的对象
      • 本地方法栈中JNI引用的对象
  • 判断一个对象是否可回收的过程

    • 1.找到GC Root不可达的对象, 如果没有重写finalize()或者调用过finalize(), 则将该对象加入到F-Qqueue中
    • 2.再次进行标记, 如果此时对象还未与GC Roots建立引用关系, 则被回收

b.回收对象引用类型

  • 强引用
    • 垃圾回收器绝对不会回收它, 当内存不足时宁愿跑出OOM错误,使得程序异常停止
  • 软引用
    • 垃圾回收期在内存充足的时候不会回收它, 而在内存不足时会回收它
    • 软引用非常适合于创建缓存. 当系统内存不足的时候,缓存中的内容是可以被释放的.
  • 弱引用
    • 垃圾回收器在扫描到该对象时,无论内存充足与否, 都会回收该对象的内存
      • ThreadLocal的key是弱引用
  • 虚引用
    • 如果一个对象只具有虚引用, 那么它和没有任何引用一样, 任何时候都可能被回收.
    • 虚引用主要用来跟踪对象被垃圾回收器回收的活动.


3.3 如何回收(垃圾收集算法)

标记-清除算法

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

具体过程如下图所示:
image.png
缺点: 容易产生内存碎片, 碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作

复制算法

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

具体过程如下图所示:
image.png

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

标记-整理算法

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

具体过程如下图所示:
image.png

分代收集算法

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

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

image.png
由如下参数控制提升阈值 -XX:+MaxTenuringThreshold=15

老年代默认都是存活对象,采用移动方式:
1. 标记所有通过 GC roots 可达的对象;
2. 删除所有不可达对象;
3. 整理老年代空间中的内容,方法是将所有的 存活对象复制,从老年代空间开始的地方依 次存放。

持久代/元数据区
1.8之前 -XX:MaxPermSize=256m
1.8之后 -XX:MaxMetaspaceSize=256m


3.4 垃圾收集器有哪些

  • Serial收集器:单线程收集器。收集垃圾时必须暂停其他所有工作线程,直到它收集结束。
  • Parnew收集器:Serial收集器多线程版本。
  • Parallel Scavenge收集器:使用复制算法的新生代收集器。
  • Serial Old收集器:使用标记-整理算法的老年代单线程收集器
  • Parallel Old收集器:使用标记-整理算法的老年代多线程收集器。
  • CMS收集器:基于标记-清除算法的低停顿并发收集器。运作步骤为①初始标记②并发标记③重新标记④并发清除。
  • G1收集器:最前沿的面向服务端应用的垃圾收集器。运作步骤为①初始标记②并发标记③最终标记④筛选回收。G1收集器有以下特点
    • 并行与并发:无需停顿Java线程来执行GC动作。
    • 分代收集:可独立管理整个GC堆。
    • 空间整合:运行期间不会产生内存空间碎片。
    • 可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。

3.5 Minor GC与Full GC

  • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代 GC(Major GC / Full GC):指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢10倍以上。
  • Minor GC与Full GC触发条件:
    • 当Eden区没有足够的空间进行分配时
    • 老年代最大可用连续空间大于Minor GC历次晋升到老年代对象的平均大小
  • Full GC触发条件:
    • 调用System.gc()时(系统建议执行Full GC,但是不必然执行)
    • 老年代空间不足时
    • 方法区空间不足时
    • 老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
    • CMS GC在垃圾回收的时候,当对象从Eden区进入Survivor区,Survivor区空间不足需要放入老年代,而老年代空间也不足时