1. 简介
垃圾收集(Garbage Collection)简称GC。JVM中, 程序计数器, 虚拟机栈, 本地方法栈都是随线程而生随线程而灭, 栈帧随着方法的进入和退出做入栈和出栈操作, 实现了自动的内存清理, 因此, 我们的内存垃圾回收主要集中于 java 堆和方法区中。
2. 如何判断对象是否存活
由于Java堆中存储着近乎所有的对象实例,所以GC在对堆进行回收之前,第一件事就是要确定回收的对象的存活状态。下面介绍几个判断对象是否存活的方法:
2.1 引用计数算法
引用计数法一般是:在对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就加1,当引用失效时,计数器值就减1,所以任何时刻计数器值为0的对象就是不再被引用的。
但是引用计数法有一个弊端,单纯的引用计数法很难解决对象之间的相互引用问题。
2.2 可达性算法分析
目前主流的语言都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路是通过一系列被成为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径成为引用链(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,则证明该对象是不可能再被使用的。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
● 在虚拟机栈(栈帧中的本地变量表)中引用的对象,例如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
● 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
● 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
● …………
2.3 引用类型
强引用:创建一个对象并把这个对象赋值给一个引用变量。内存不够不会回收对象。
软引用:SoftReference,内存空间够的话GC不会回收。用于网页缓存,图片缓存等。
弱引用:WeakReference,JVM进行垃圾回收时,无论内存,都会回收弱引用关联的对象。
虚引用:PhantomReference,不影响生命周期,会在回收时,通过关联的队列,得知与其关联的对象的垃圾回收状态。
3. 垃圾收集算法
3.1 分代收集理论
分代收集理论基于两个假说:弱分代假说(绝大多数对象都是朝生夕灭的),强分代假说,(熬过越多次垃圾收集的对象越难消亡)。所以最终总结出,收集器应该将Java堆分为不同的区域,然后将回收对象依据其年龄分配到不同的区域存储(新生代、老年代)。同时还提出了跨代引用假说(存在互相引用的两个对象,应该倾向于同时生存或者同时消亡的)来解决新生代收集(Minor GC)时,存在新生代对象被老年代所引用的情况。
3.2 标记-清除算法
标记-清除(Mark-Sweep)算法是最基础的GC算法,如同他的名字一样,算法分为标记和清除两个阶段,首先标记需要回收的对象,再统一回收掉标记的对象。
缺点:
(1)执行效率不稳定。若对象数量及回收对象的数量过于大,遍历标记清除的效率会很低。
(2)内存空间的碎片化问题。标记清除后会存在大量不连续的内存空间,空间碎片太多会导致以后的程序运行时如果要分配较大对象时无法找到足够连续多的内存而出发另一次GC。
3.3 标记-复制算法
为了解决标记-清除算法的执行效率低的问题,Fenichel提出了一种被称为半区复制的垃圾收集算法,它将可用内存按容量划分为大小相同的两个部分,每次只使用其中的一个部分。当这一部分用完了时,就将还活着的对象复制到另一部分内存中,然后再把已使用过的内存部分一次性清理掉。
缺点:
(1)内存空间浪费
3.4 标记-整理算法
标记-复制算法在对象存活率较高的内存区域进行时,效率会降低。更关键的是,如果不想浪费50%的内存空间,所以就需要有额外的空间进行分配担保,以应对100%对象都存活的极端情况,所以在老年代,一般不推荐用标记-复制算法。
针对老年代的对象的存亡特征,Edward Lueders提出了另一种有针对性的标记-整理算法(Mark-Compact),其中的标记过程仍然与标记-清除算法相同,但后续的步骤不是对可回收的对象进行回收操作,而是让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。
缺点:
(1)移动存活对象,是一种较为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。
4. 垃圾收集器
4.1 Serial收集器
Serial收集器是一个单线程工作的收集器,它在垃圾收集时,必须暂停其他所有工作线程(Stop The World),直到它收集结束。
简单而高效(和其他收集器的单线程相比),对于内存受限的场景,它是所有收集器里面额外内存消耗最小的。
4.2 ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本。
ParNew 追求"低停顿时间", 与 Serial 唯一的区别就是使用了多线程进行垃圾收集, 在多 CPU 环境下性能比 Serial 会有一定的提升; 但线程切换需要额外开销, 因此在单 CPU 环境下表现不如 Serial.
4.3 Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器,他的特点是,Parallel Scavenge收集器关注的是达到一个可控制的吞吐量,所谓吞吐量就是处理器用于处理用户代码与处理器总消耗时间的比值。
4.4 Serial Old收集器
Serial 的老年代版本, 都是单线程收集器, 只启用一条 GC 线程.
和 Serial 的区别: Serial Old 工作在老年代, 使用 "标记-整理"算法; Serial 工作在新生代, 使用复制算法.
4.5 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
4.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,他的运作过程整体可以分为四个部分:
(1)初始标记
标记一下GC Roots能够关联到的对象,需要Stop The World。
(2)并发标记
从GC Roots的直接关联对象开始遍历整个对象图的过程,整个过程耗时比较长但是不会影响到用户的正常操作线程,可以和垃圾回收线程并发运行。
(3)重新标记
修正并发标记期间,因用户程序还在运行导致标记产生变动的那一部分对象的标记记录,需要Stop The World。
(4)并发清除
清除删除掉标记阶段判断的已经死亡的对象,可以并发进行。
由于在并发标记和并发清除两个耗时比较大的阶段可以和用户线程并发进行,所以CMS收集器可以有效地减少等待与停顿的时间。
缺点:
(1)由于并发进行,会减少吞吐量
(2)无法处理浮动垃圾,容易频繁导致Full GC
(3)收集结束会产生大量的空间碎片
4.7 Garbage First收集器(G1)
G1收集器是目前来说比较好的一个收集器,它开创了收集器面向局部收集的设计思路以及基于Region的内存布局格式。
它没有新生代和老年代的概念, 而是将堆划分为一块块独立的 Region. 当要进行垃圾收集时, 首先计算每个 Region 中垃圾的数量, 每次都从垃圾回收价值最大的 Region 开始回收, 因此可以获得最大的回收效率.
从整体上看, G1 是基于"标记-整理"算法实现的收集器, 从局部 (两个 Region 之间) 上看是基于"复制"算法实现的, 这意味着运行期间不会产生内存碎片.
每个 Region 中都有一个 Remembered Set, 用于记录本区域中所有对象的引用所在的区域, 进行可达性分析时, 只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历.
如果不计算维护 Remembered Set 的操作, G1 收集器的工作过程可以分为一下几个步骤:
(1)初始标记: Stop The World, 仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记.
(2)并发标记: 使用一条标记线程, 与用户线程并发执行. 此过程进行可达性分析, 标记出所有废弃对象, 速度很慢.
(3)最终标记: Stop The world, 使用多条线程并发执行.
(4)筛选回收: 回收废弃对象, 此时也要 Stop The world, 并使用多条筛选回收线程并发执行.