1.概念
垃圾收集器(Garbage Collection,GC),回收的条件需要考虑以下
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
2.确定回收对象
回收的对象一般是不被引用的无用对象,那么如何确定对象没有被引用了呢?
2.1.引用计数算法
基本思想:给对象添加一个计数器,每当有一个地方引用它时,计数器的值就加1,当引用失效时,计数器就减1,在任何时刻计数器为0的对象就不可能再被引用。
- 优点: 这个判定效率很高,实现也简单
- 缺点: 有些情况不能处理,例如两个没有其他引用的两个对象互相引用,就造成了循环引用的问题。
2.2.可达性分析算法
基本思想:通过一些列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots 没有任何引用链相连,则证明这个对象不可用的。
a1到a3为可用对象,a4-a6为回收对象
在Java中可以作为GC Roors 的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中的JNI(即Native方法)引用的对象。
3.垃圾收集算法
3.1.标记-清除算法
算法分为两个阶段:
- 标记:首先标记出所需要回收的对象
- 回收:在标记完成后统一回收所有被标记的对象
缺点:
- 效率低,标记和清除效率都不高
- 空间零碎,标记清除后会产生大量零碎空间,导致分配大对象时不得不提前触发另一次垃圾回收机制
3.2. 复制算法
为解决标记-清除算法效率低下的问题,复制算法出现了
基本思想:将内存分为两块等大的空间,每次只是用一块,当这块空间用完了,就把还存活的对象复制到另一块上去,然后整块回收。
缺点:将内存分为了两半,使可用内存急剧减少
优化:每次分为两等份,存放活对象的区域会有浪费,年轻代细分把内存区域分成Eden、From survivor 和 To survivor ,比例为8:1:1,复制算法一般用于年轻代,From survivor 和 To survivor 简称为S0区和S1区,优化如下:
- Eden + S0区 用于存储全部对象,S1区用于存储GC活下来的对象,回收Eden + S0区
- Eden + S1区 用于存储全部对象,S0区用于存储GC活下来的对象,回收Eden + S1区
- 循环
这样的优点就是可以把可用内存控制在 9 / 10 ,提高内存利用率
年轻代为什么设置成三个块?
答:解决了内存碎片问题。
3.3.标记-整理算法
复制算法针对所有对象都存活的极端情况下是不可取的,会造成两个内存块之间不断切换,浪费资源。
标记-整理思想:先对存活对象进行标记,将存活对象都向一端移动,然后清理掉边界以外的内存。
3.4.分代收集算法
分代收集的思想是将堆中的对象分成两种情况:新生代和老年代。然后再根据每个分区实行不同的算法。新生代每次都伴有大量对象被回收,故采用复制算法即可达到最大效果,老年代存活的对象较多,采用“标记-整理”或者“标记-清理”算法比较合适
3.5.minor GC、major GC和full GC
在HotSpot虚拟机中存在三种垃圾回收现象,minor GC、major GC和full GC。对新生代进行垃圾回收叫做minor GC,对老年代进行垃圾回收叫做major GC,同时对新生代、老年代和永久代进行垃圾回收叫做full GC。许多major GC是由minor GC触发的,所以很难将这两种垃圾回收区分开。major GC和full GC通常是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是major GC。
4.垃圾收集器
-
串行垃圾回收器(Serial Garbage Collector)
-
并行垃圾回收器(Parallel Garbage Collector)
-
并发标记扫描垃圾回收器(CMS Garbage Collector)
-
G1垃圾回收器(G1 GarbageCollector)
HotSpot虚拟机的垃圾收集器
如果说垃圾回收算法是内存回收的方法论,那垃圾回收器就是内存回收的具体实现
新生代收集器
4.1.Serial 收集器使用“复制”算法
这是一个单线程的收集器,但它并不仅仅使用一个cpu或一个线程去收集,当它进行垃圾回收的时候,必须停止其他工作线程,sun公司把这种情况称为“Stop The World” , 但是这种停顿对用户来说体验非常差,虽然描述的Serial 收集器很鸡肋,但是它依然是虚拟机运行在Client模式下的默认新生代收集器。
- 优点:简单高效(与其他收集器的单线程相比),对于限定单cpu来说,它没有其它线程交互,一心一意捡垃圾。一般在用户桌面应用场景中,分配给虚拟机管理的内存不会很大,每次收集几十兆几百兆的新生代停顿时间一般控制在一百毫秒作用,这对用户来说影响不大。
4.2 ParNew 收集器使用“复制”算法
ParNew其实就是 Serial 收集器的多线程版本,与Serial相比并没有太多的改变,唯一重要的一点是它能与CMS(稍后介绍这款收集器)收集器协同工作,CMS是收集线程和用户线程同时工作,实现了并发收集器,也就是没有Stop The World 。
4.3 Parallel Scavenge 收集器使用“复制”算法
它也使用复制算法,并行的多线程收集器,这些都和ParNew收集器一样。但它关注的是吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间的比值,虚拟机运行100分钟,其中垃圾收集用掉1分钟,吞吐量就是99%),而其它收集器(Serial/Serial Old、ParNew、CMS)关注的是垃圾收集时用户线程的停顿时间。
老年代收集器
4.4 Serial Old 收集器使用“标记-整理”算法
他是一个单线程收集器,主要意义也是给Client模式下虚拟机使用。
4.5 Parallel Old 收集器使用“标记-整理”算法
它是一个多线程收集器,是Paeallel Scavenge的老年代版本,可与Paeallel Scavenge组合,组成“吞吐量优先”的应用组合。
4.6 CMS 收集器使用“标记-清除”算法
CMS(Concurrent Mark Sweep) ,收集器是一种以最短回收停顿时间为目标的收集器。
它的运行过程分为四个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记和重新标记仍需要 Stop The World ,初始标记仅仅是简单地标记一下能直接关联到的对象,并发标记就是进行GC Roots Tracing 的过程了,重新标记就是修正并发标记期间用户程序继续运行程序而产生的一些对象变动,这个阶段停顿时间比初始标记时间稍长,但是明显比并发标记时间短。
因为耗时最长的并发标记是与用户程序并发执行的,所以CMS是一款低停顿的收集器。
缺点:
- CMS 收集器对CPU资源非常敏感。并发就会对CPU造成负担
- CMS 收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致出现另一次 Full GC 产生。
- “标记-清除”算法产生的大量零碎空间碎片。
4.7 G1 收集器使用“标记-整理”算法
特点:
- 并行与并发
- 分代收集:采用与其他GC不同的分代形式,采用不用的方式去处理新建对象和已经存活一段时间的对象,对熬过多次GC的对象比较友好
- 空间整合:G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)来看是“复制”算法的收集器,无论哪种算法都意味着没有碎片空间产生,这种算法有利于程序长时间运行,当分配大对象的时候不会因无大空间而触发GC
- 可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片内GC的时间不会超过N毫秒。
虽然G1分代,但是他不再是局限于新生代和老年代这两个区域,而是将整个Java堆分成多个大小相等的独立区域,新生代和老年代的概念被放入了这多个区域里,这些区域不再是物理隔离,G1之所以能预测停顿,是因为他在有计划地避免整个Java堆进行全区域GC。
这样就真的是以Region为单位进行GC了吗?Region不能孤立工作,对象之间或多或少有些联系,不可能所有有联系的对象都预先分配到一起,这是不可能的,所以只扫描一个Region不能确认对象是否存活。
G1收集器的运作大致可以分为:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收