垃圾回收(GC)是由 Java 虚拟机(JVM)垃圾回收器提供的一种对内存回收的一种机制,它一般会在内存空闲或者内存占用过高的时候对那些没有任何引用的对象不定时地进行回收。
什么是垃圾回收机制
垃圾回收机制(简称GC)专门用来回收不可用的变量值所占用的内存空间。
为什么要用垃圾回收机制
程序运行过程中会申请大量的内存空间,而对于一些无用的内存空间如果不及时清理的话会导致内存使用殆尽(内存溢出),导致程序崩溃。
内存中的垃圾
程序在运行过程中会创建对象,但是当方法执行完成或当这个对象使用完毕之后,被定义为垃圾。判定一个对象是否是垃圾,即判定一个对象的存活与否,常见的算法有两种:引用计数法 和 根搜索算法。
① 引用计数算法(Reference Counting Collector)
一个对象被创建之后,系统会给这个对象初始化一个引用计数器,当这个对象被引用了,则计数器 +1,而当该引用失效后,计数器便 -1,直到计数器为 0,意味着该对象不再被使用了,则可以将其进行回收了。
这种算法其实很好用,判定比较简单,效率也很高,但是却有一个很致命的缺点,就是它无法避免循环引用,即两个对象之间循环引用的时候,各自的计数器始终不会变成 0,所以 引用计数算法 只出现在了早期的 JVM 中,现在基本不再使用了。
当程中出现序循环引用时,引用计数算法无法检测出来,被循环引用的内存对象就成了无法回收的内存。从而引起内存泄露
。
class A{
public B b;
}
class B{
public A a;
}
public class Main{
public static void main(String[] args){
A a = new A();
B b = new B();
a.b=b;
b.a=a;
}
}
② 根搜索算法(Tracing Collector)
根搜索算法的中心思想(可达性分析算法),以一系列被称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明该对象不再存活可以作为垃圾被回收。这种算法很好地解决了上面 引用计数算法 的循环引用的问题了。
算法的核心思想是很简单的,就是标记不可达对象,然后交由 GC 进行回收,但是有一个点是很重要的,那就是 何为根对象(GC Roots)?
根对象,一般有如下几种:
不可达的对象一定会被回收吗?
即使在根搜索算法(可达性分析算法)判断不可达的对象,也并非是 " 非死不可的 "。
如果对象在进行根搜索算法后发现没有与GC Roots相连的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或finalize()方法已经被调用过,finalize()方法都不会执行,该对象将会被回收。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列中,然后由Finalizer线程去执行。GC将会对F-Queue中的对象进行第二次标记,如果对象finalize()方法中重新与引用链上的任何一个对象建立关联,那么在第二次标记时将会被移除 " 即将回收 " 的集合,否则该对象将会被回收。
回收算法
除了标记 " 垃圾对象 " 的算法,我们也需要 " 清理垃圾 "的回收算法。常用的回收算法一般有:标记-清除算法、标记-整理算法、复制算法,以及系统自动进行判定使用的 适应性算法。
① 标记 - 清除算法(Tracing Collector)
标记-清除 算法是最基础的收集算法,它是由 标记 和 清除 两个步骤组成的。第一步是标记存活的对象,第二步是清除没有被标记的垃圾对象。
该算法的优点是当存活对象比较多的时候,性能比较高,因为该算法只需要处理待回收的对象,而不需要处理存活的对象。但是缺点也很明显,清除之后会产生大量不连续的内存碎片。导致之后程序在运行时需要分配较大的对象时,无法找到足够的连续内存。
② 标记 - 整理算法(Compacting Collector)
上述的 标记-清除 算法会产生内存区域使用的间断,所以为了将内存区域尽可能地连续使用, 标记-整理 算法应运而生。标记-整理 算法也是由两步组成,标记 和 整理。
和标记清除算法一样,先进行对象的标记,通过GC Roots节点扫描存活对象进行标记,将所有存活对象往一端空闲空间移动,按照内存地址依次排序,并更新对应引用的指针,然后清理末端内存地址以外的全部内存空间。
但是同样,标记整理算法也有它的缺点,一方面它要标记所有存活对象,另一方面还添加了对象的移动操作以及更新引用地址的操作,因此标记整理算法具有更高的使用成本。
③ 复制算法(Copying Collector)
无论是标记-清除算法还是垃圾-整理算法,都会涉及句柄的开销或是面对碎片化的内存回收,所以,复制算法 出现了。
复制算法将内存区域均分为了两块(记为S0和S1),而每次在创建对象的时候,只使用其中的一块区域(例如S0),当S0使用完之后,便将S0上面存活的对象全部复制到S1上面去,然后将S0全部清理掉。复制算法主要被应用于新生代,它将内存分为大小相同的两块,每次只使用其中的一块。在任意时间点,所有动态分配的对象都只能分配在其中一个内存空间,而另外一个内存空间则是空闲的。
但是缺点也是很明显的,可用的内存减小了一半,存在内存浪费的情况。所以 复制算法 一般会用于对象存活时间比较短的区域,例如 年轻代,而存活时间比较长的 老年代 是不适合的,因为老年代存在大量存活时间长的对象,采用复制算法的时候会要求复制的对象较多,效率也就急剧下降,所以老年代一般会使用上文提到的 标记-整理算法。
④ 分代收集算法
实际上,java中的垃圾回收器并不是只使用的一种垃圾收集算法,当前大多采用的都是分代收集法。一般根据对象存活周期的不同,将内存分为几块,一般是把堆内存分为新生代和老年代,再根据各个年代的特点选择最佳的垃圾收集算法。主要思想如下:
至于为什么在某一区域选择某种算法,还是和三种算法的特点息息相关的,再从3个维度进行一下对比:
尽管具有很多差异,但是除了都需要进行标记外,还有一个相同点,就是在gc线程开始工作时,都需要STW暂停所有工作线程。
七种经典的垃圾回收器
按工作的内存区间分,可分为年轻代垃圾回收器和老年代垃圾回收器。
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使用 |
Parallel | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行 | 老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 适用于单CPU环境下的Client模式 |
Paraller Old | 并行 | 老年代 | 标记-整理(压缩)算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
CMS | 并发 | 老年代 | 标记-清除算法 | 响应速度优先 | 适用于互联网或B/S业务 |
G1 | 并发、并行 | 新生代、老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 响应速度优先 |