本文从JVM如何判定对象是否需要回收开始分析,再到JVM的几种垃圾回收思想如何产生,最后再来介绍JVM经典的7种垃圾回收器的特点(不包含ZGC);
JVM根据对象存活周期不同将heap划分成了新生代、老年代、永久代(方法区&元空间)。
有个问题,JVM是先有的分代思想然后根据不同的代发展不同的垃圾回收思想,还是先有的垃圾回收思想才划分不同的代?
JAVA与C有个很显著的不同,就是JAVA不需要手动 归还 内存,完全由GC自动管理内存回收。 那么GC是如何判断对象是否需要回收的呢?
引用计数法是指在对象中添加一个引用计数器,如果被其他对象引用则计数器+1,引用失效时-1。
优点:实现简单,判断效率也很高;
缺点:存在对象循环引用问题,所以在主流的虚拟机中并没有采用引用计数器。
对象A持有对象B的引用,对象B持有对象A的引用,除此之外在无其他对象引用A和B,GC无法回收这样的对象.
在主流商用语言(JAVA/C#/Lisp)都是使用可达性分析算法来判定对象是否存活。主要思想就是通过一系列被称为 GC Roots
的对象作为起始点开始先下搜索,走过的路径称为引用链,如果某个对象没有任何一条到达 GC Roots
对象的引用链则代表此对象可回收的。
JAVA中可以被称为 GC Roots
对象:
GC Roots
无法到达的对象并不是一定会被回收,一个对象至少要被标记两次才会真正死亡。
GC Roots
时会被第一次标记,并进行一次筛选,筛选的条件是该对象是否有必要执行 finalize()
。 finalize()
方法 finalize()
方法 finalize()
方法; finalize()
方法里关联上任何一条引用链,则会被移出即将要回收的集合,否则该对象真正“死亡”。 在JVM知道那些对象是可回收的后,需要开始真正的回收对象了。JVM在发展的过程中出现了几种经典的回收思想,这里不讨论每种算法具体如何实现(因为我也不了解...)。
标记-清除算法
JVM分配内存时整个heap可以看做一个大的表格里有多个单元格,对于要回收的对象打上一个“标记”,然后对标记的对象进行“清除”,“标记-清除”也是最基础的思想,后面的几种思想都是基于这之上的改进。
缺点:复制算法
为了解决效率问题,出现了一种复制的算法,一开始是将内存按1:1划分成两块,每次只在其中一块内存上分配对象,当触发垃圾回收时将存活的对象全部复制到另一块的内存上,然后把已经使用过的那快内存清空掉。这样既解决了效率问题也解决了内存碎片化的问题。 但同时也带来了空间浪费的缺点:每次只能使用50%的空间 。
后来IBM有专门研究新生代的对象大多朝夕生死(创建后很快会销毁),所以并不需要按1:1来分配,而是按8:1:1来划分,一块较大的Eden空间和两块较小的Survivor空间, 每次分配占用Eden+一块Survivor空间(新对象的分配只会在Eden上),当垃圾回收时将存活的对象拷贝到另一块Survivor ,这样空间利用率达到90%。实际情况并不是每次回收时一块Survivor都能装下所有存活对象,那这时就会通过“空间分配担保”的机制直接晋升到老年代。
标记-整理算法
由于老年代的对象都是长期存活,所以复制算法并不适用老年代,因此又提出了“标记-整理”算法,标记过程与“标记-清除”算法一样,只是后续并不是直接清除对象而是先将所有存活对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
当前主流商用垃圾回收器都是采用的“分代收集算法”,这个算法并没有什么新的思想只是根据对象存活周期的不同将内存划分成不同的代然后采用不同的回收算法。
黄色代表只处理新生代的GC,蓝色代表只处理老年代GC,各GC之间的连线代表可以搭配使用。G1可以独立回收整个head;
在介绍这些收集器各自的特性之前,让我们先来明确一个观点:虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,HotSpot虚拟机完全没必要实现那么多种不同的收集器了(摘选自《深入理解Java虚拟机(第2版)》)。
Serial
Serial收集器是最基本历史最悠久的收集器,JDK1.3.1之前是新生代唯一的选择。Serial是一个单线程收集器,这里的“单线程” 并不是指一个CPU或一条线程 而是Serial在垃圾收集时必须暂停其他工作线程(Stop The World)也就是俗称的“STW”。
ParNew
ParNew收集器是Serial收集器的多线程版本,除使用多条线程进行垃圾收集之外,其余行为包括控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
CMS
搭配使用,多核CPU情况下能有效利用系统资源。
Parallel Scavenge
Parallel Scavenge收集器是一个并行的多线程年轻代收集器,其他收集器关心如何缩短垃圾收集的时间而它关注的是如何控制系统运行的吞吐量( 吞吐量(吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间))
)。高吞吐量可以高效率的利用CPU时间,尽快完成运算任务,只要适合在后台运算而不需要太多交互的任务。
Serial Old
Serial的老年代版本,它也是一款使用"标记-整理"算法的单线程的垃圾收集器,优劣和Serial一样。有两大用途:
Concurrent Mode Failure
情况下老年代预备方案
Parllel Old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用"标记-整理"算法。JDK1.6才提供,在此之前Parallel Scavenge只能和单线程的Serial Old搭配使用,由于老年代的Serial Old在服务端拖累又不能有效利用多核CPU的处理能力,导致Parallel Scavenge的高吞吐名副其实。直到Parllel Old的出现“吞吐量优先”的收集器才有了用武之地,任何注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器一起配合使用
CMS
真正意义上的一款具有划时代意义的垃圾收集器,基于“标记-清除”算法实现,关注点在获取最短停顿时间为目标,大量运用在B/S系统的服务端上。
整个回收过程分为四个步骤:
标记 GC Roots
能 直接 关联到的对象,速度很快。需要STW
标记 GC Roots
找到 所有 能关联到的对象
因为 并发标记
是和用户线程并发的所以在标记的过程中会产生新的对象,所以要重新标记。需要STW
并发清除前面所有标记的对象。
G1
G1全称“Garbage First”垃圾收集器直至JDK7,Sun公司才认为G1达到足够成熟的商用程度,目标是在未来可以替换掉CMS。之前的GC都只负责整个新生代/老年代,而G1可以独立负责整个Heap,G1是将整个Heap划分成多个大小相等的Region,逻辑上仍保留分代的概念,但已不是物理分隔了,它们都是一部分不需要连续的Region集合。
G1有以下特点:
指定一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不超过N毫秒
)。G1对每一个Region的垃圾堆积的价值大小维护了一个优先列表,每次根据允许的收集时间,优先回收价值大的Region(这就是Garbage First名称的由来),保证了有限的时间内获取尽可能高的收集效率。 前三个步骤与CMS运作过程大致相似,