GC的区域为方法区和堆 其中最主要的区域是堆
对于程序计数器、Java虚拟机栈、本地方法栈GC是不负责的
虽然GC不负责这三块区域 但不代表他们不会发生内存溢出的问题

一、垃圾回收的意义

实际上 Java技术体系中所提倡的 自动内存管理 最终可以归结为自动化地解决了两个问题:给对象分配内存 以及 回收分配给对象的内存
而且这两个问题针对的内存区域就是Java内存模型中的堆区
垃圾回收机制是Java语言一个显著的特点 其可以有效的防止内存泄露、保证内存的有效使用 从而使得Java程序员在编写程序的时候不再需要考虑内存管理问题
Java 垃圾回收机制要考虑的问题很复杂 主要掌握三个核心问题
1)那些内存需要回收?(对象是否可以被回收的两种经典算法: 引用计数法 和 可达性分析算法)
2)**什么时候回收? (堆的新生代、老年代、元空间的垃圾回收时机,MinorGC 和 FullGC)
3)如何回**收?(三种经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法 和 七种垃圾收集器)

首先应该记住一个单词:Stop-the-World
Stop-the-world意味着 JVM由于要执行GC而停止了应用程序的执行 并且这种情形会在任何一种GC算法中发生
当Stop-the-world发生时 除了GC所需的线程以外 所有线程都处于等待状态直到GC任务完成
事实上 GC优化很多时候就是指减少Stop-the-world发生的时间 从而使系统具有 高吞吐 、低停顿 的特点

Ps:内存泄露是指该内存空间使用完毕之后未回收
在不涉及复杂数据结构的一般情况下 Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度

二、如何确定一个对象是否可以被回收?

1 引用计数算法:判断对象的引用数量(不是Java使用的)

引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收

引用计数算法是垃圾收集器中的早期策略
在这种方法中 堆中的每个对象实例都有一个引用计数
当一个对象被创建时 且将该对象实例分配给一个引用变量 该对象实例的引用计数设置为 1
当任何其它变量被赋值为这个对象的引用时 对象实例的引用计数加 1(a = b 则b引用的对象实例的计数器加 1)
但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时 对象实例的引用计数减 1
特别地 当一个对象实例被垃圾收集时 它引用的任何对象实例的引用计数器均减 1 任何引用计数为0的对象实例可以被当作垃圾收集

引用计数收集器可以很快的执行 并且交织在程序运行中 对程序需要不被长时间打断的实时环境比较有利
但其很难解决对象之间相互循环引用的问题
如下图所示 对象objA和objB之间的引用计数永远不可能为 0 那么这两个对象就永远不能被回收
IHUTJVMT(SGB_AJWQ})DY0W.jpg

  1. public class ReferenceCountingGC {
  2.   
  3. public Object instance = null;
  4. public static void testGC(){
  5. ReferenceCountingGC objA = new ReferenceCountingGC ();
  6. ReferenceCountingGC objB = new ReferenceCountingGC ();
  7. // 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为 0
  8. objB.instance = objA;
  9. objA.instance = objB;
  10. objA = null;
  11. objB = null;
  12. System.gc();
  13. }
  14. }

上述代码最后面两句将objA和objB赋值为null 也就是说objA和objB指向的对象已经不可能再被访问
但是由于它们互相引用对方 导致它们的引用计数器都不为 0 那么垃圾收集器就永远不会回收它们

2 可达性分析算法:判断对象的引用链是否可达(Java使用的)

可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收

可达性分析算法是从离散数学中的图论引入的
程序把所有的引用关系看作一张图 通过一系列的名为 “GC Roots” 的对象作为起始点 从这些节点开始向下搜索 搜索所走过的路径称为引用链(Reference Chain)
当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时 则证明此对象是不可用的
如下图所示
![W@AL7T}A@}IGG5{}$`TWD8.jpg
在Java中 可作为 GC Root 的对象包括以下几种:
1)虚拟机栈(栈帧中的局部变量表)中引用的对象
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中Native方法引用的对象

三、垃圾收集算法

1 标记清除算法

标记-清除算法分为标记和清除两个阶段
该算法首先从根集合进行扫描 对存活的对象对象标记
标记完毕后 再扫描整个空间中未被标记的对象并进行回收
如下图所示
ZWGU9J(CFPU5F4}H8H_V4IV.jpg
标记-清除算法的主要不足有两个:
1)效率问题:标记和清除两个过程的效率都不高
2)空间问题:标记-清除算法不需要进行对象的移动 并且仅对不存活的对象进行处理
因此标记清除之后会产生大量不连续的内存碎片
空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时 无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
如下图
KB06KVJ%OD}5FK__@_H9{YO.jpg

2 复制算法

复制算法将可用内存按容量划分为大小相等的两块 每次只使用其中的一块
当这一块的内存用完了 就将还存活着的对象复制到另外一块上面 然后再把已使用过的内存空间一次清理掉
这种算法适用于对象存活率低的场景 比如新生代
这样使得每次都是对整个半区进行内存回收 内存分配时也就不用考虑内存碎片等复杂情况
只要移动堆顶指针 按顺序分配内存即可 实现简单 运行高效
该算法示意图如下所示:
]4VJ~_9KYQ70_(8(E9G5S]C.jpg
事实上 现在商用的虚拟机都采用这种算法来回收新生代
因为研究发现 新生代中的对象每次回收都基本上只有10%左右的对象存活 所以需要复制的对象很少 效率还不错
新生代内存会分为一块较大的Eden空间和两块较小的Survivor空间 每次使用Eden和其中一块Survivor
当回收时 将Eden和Survivor中还存活着的对象一次地复制到另外一块Survivor空间上 最后清理掉Eden和刚才用过的Survivor空间
HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1:1
也就是每次新生代中可用内存空间为整个新生代容量的90% ( 80%+10% ) 只有10% 的内存会被“浪费”

3 标记整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作 效率将会变低
更关键的是 如果不想浪费50%的空间 就需要有额外的空间进行分配担保 以应对被使用的内存中所有对象都100%存活的极端情况
所以在老年代一般不能直接选用复制收集算法
标记整理算法的标记过程类似标记清除算法
但后续步骤不是直接对可回收对象进行清理 而是让所有存活的对象都向一端移动
然后直接清理掉端边界以外的内存 类似于磁盘整理的过程 该垃圾回收算法适用于对象存活率高的场景(老年代)
其作用原理如下图所示
4CAY6OMCIO](VT`WP90ATMS.jpg
标记整理算法与标记清除算法最显著的区别是:
标记清除算法不进行对象的移动 并且仅对不存活的对象进行处理
而标记整理算法会将所有的存活对象移动到一端 并对不存活对象进行处理 因此其不会产生内存碎片

4 分代收集算法(Java所采用的)

对于一个大型的系统 当创建的对象和方法变量比较多时 堆内存中的对象也会比较多 如果逐一分析对象是否该回收 那么势必造成效率低下
分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况)是不一样的 而不同生命周期的对象位于堆中不同的区域
因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率
当代商用虚拟机使用的都是分代收集算法:
1)新生代对象存活率低 就采用复制算法
2)老年代存活率高 就用标记清除算法或者标记整理算法

四、垃圾收集器

如果说垃圾收集算法是内存回收的方法论 那么垃圾收集器就是内存回收的具体实现
下图展示了7种作用于不同分代的收集器
其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge
回收老年代的收集器包括Serial Old、Parallel Old、CMS
还有用于回收整个Java堆的G1收集器
不同收集器之间的连线表示它们可以搭配使用
BMBMFPDV]Y}2IZJ8497B3SU.jpg

1 新生代收集器

1)Serial收集器(复制算法)
Serial 是一款用于新生代的单线程收集器 采用复制算法进行垃圾收集
Serial 进行垃圾收集时 不仅只用一条线程执行垃圾收集工作
它在收集的同时 所有的用户线程必须暂停(Stop The World)

如下是 Serial 收集器和 Serial Old 收集器结合进行垃圾收集的示意图
当用户线程都执行到安全点时 所有线程暂停执行
Serial 收集器以单线程 采用复制算法进行垃圾收集工作 收集完之后 用户线程继续开始执行
![W{8F]K@1VP_YA@I@S%G14C.jpg
适用场景:Client 模式(桌面应用);单核服务器
可以用 -XX:+UserSerialGC 来选择 Serial 作为新生代收集器
2)ParNew 收集器(复制算法)
ParNew 就是一个 Serial 的多线程版本 其它与Serial并无区别
ParNew 在单核 CPU 环境并不会比 Serial 收集器达到更好的效果
它默认开启的收集线程数和 CPU 数量一致
可以通过 -XX:ParallelGCThreads 来设置垃圾收集的线程数

如下是 ParNew 收集器和 Serial Old 收集器结合进行垃圾收集的示意图
当用户线程都执行到安全点时 所有线程暂停执行
ParNew 收集器以多线程 采用复制算法进行垃圾收集工作 收集完之后 用户线程继续开始执行
]PQL7FDTB`5JNZB@ABN]X9R.jpg
适用场景:多核服务器 与 CMS 收集器搭配使用
当使用 -XX:+UserConcMarkSweepGC 来选择 CMS 作为老年代收集器时 新生代收集器默认就是 ParNew
也可以用 -XX:+UseParNewGC 来指定使用 ParNew 作为新生代收集器
3)Parallel Scavenge 收集器(复制算法)

Parallel Scavenge 也是一款用于新生代的多线程收集器
与 ParNew 的不同之处是ParNew 的目标是尽可能缩短垃圾收集时用户线程的停顿时间
Parallel Scavenge 的目标是达到一个可控制的吞吐量 即追求高吞吐量 高效利用 CPU
吞吐量 = 执行用户线程的的时间/(执行用户线程的的时间+垃圾收集时间)
比如虚拟机一共运行了 100 分钟 其中垃圾收集花费了 1 分钟 那吞吐量就是 99%
![WLP1]`5COJG0KG}](P96%H.jpg
比如上面两个场景
垃圾收集器每 100 秒收集一次 每次停顿 10 秒
和垃圾收集器每 50 秒收集一次 每次停顿时间 7 秒
虽然后者每次停顿时间变短了 但是总体吞吐量变低了 CPU 总体利用率变低了

可以通过 -XX:MaxGCPauseMillis 来设置收集器尽可能在多长时间内完成内存回收
可以通过 -XX:GCTimeRatio 来精确控制吞吐量

如下是 Parallel Scavenge 收集器和 Parallel Old 收集器结合进行垃圾收集的示意图
@G9GF4Z9VU)0)~TM0}A6$EJ.jpg
适用场景:注重吞吐量 高效利用 CPU 需要高效运算且不需要太多交互
可以使用 -XX:+UseParallelGC 来选择 Parallel Scavenge 作为新生代收集器
jdk7、jdk8 默认使用 Parallel Scavenge 作为新生代收集器

2 老年代垃圾收集器

1)Serial Old 收集器(标记-整理算法)
Serial Old 收集器是 Serial 的老年代版本 同样是一个单线程收集器 采用标记-整理算法

适用场景:Client 模式(桌面应用);单核服务器;与 Parallel Scavenge 收集器搭配;作为 CMS 收集器的后备预案
2)Parallel Old 收集器(标记-整理算法)
Parallel Old 收集器是 Parallel Scavenge 的老年代版本 是一个多线程收集器 采用标记-整理算法
可以与 Parallel Scavenge 收集器搭配 充分利用多核 CPU 的计算能力

适用场景:与Parallel Scavenge 收集器搭配使用;注重吞吐量
jdk7、jdk8 默认使用该收集器作为老年代收集器
使用 -XX:+UseParallelOldGC 来指定使用 Paralle Old 收集器
3)CMS(Concurrent Mark Sweep) 收集器(标记-清除算法) 高版本的JDK已经弃用了
CMS 收集器是老年代并行收集器 是一种以最短回收停顿时间为目标的收集器 具有高并发、低停顿的特点
整个垃圾收集过程分为 4 个步骤:
① 初始标记:标记一下 GC Roots 能直接关联到的对象 速度较快
② 并发标记:进行 GC Roots Tracing 标记出全部的垃圾对象 耗时较长
③ 重新标记:标记用户程序继续运行而导致变化的对象的标记记录(并发标记和用户线程一起执行 这个阶段用户线程也可能会产生新的垃圾) 耗时较短
④ 并发清除:用标记-清除算法清除垃圾对象 耗时较长
整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作 所以从总体上来说 CMS 收集器垃圾收集可以看做是和用户线程并发执行的
6XW0U$%BICM)JRJ9T{$X6IT.jpg
CMS 收集器也存在一些缺点:
1)对 CPU 资源敏感:默认分配的垃圾收集线程数为(CPU 数+3)/4 随着 CPU 数量下降 占用 CPU 资源越多 吞吐量越小
2)无法处理浮动垃圾:在并发清理阶段 由于用户线程还在运行 还会不断产生新的垃圾 CMS 收集器无法在当次收集中清除这部分垃圾
同时由于在垃圾收集阶段用户线程也在并发执行 CMS 收集器不能像其他收集器那样等老年代被填满时再进行收集 需要预留一部分空间提供用户线程运行使用
当 CMS 运行时 预留的内存空间无法满足用户线程的需要 就会出现 “ Concurrent Mode Failure ”的错误
这时将会启动后备预案 临时用 Serial Old 来重新进行老年代的垃圾收集

适用场景:重视服务器响应速度 要求系统停顿时间最短
可以使用 -XX:+UserConMarkSweepGC 来选择 CMS 作为老年代收集器(高版本已经弃用)

3 G1(Garbage First)收集器

G1 收集器是 jdk1.7 才正式引用的新收集器 现在已经成为 jdk9 默认的收集器
前面几款收集器收集的范围都是新生代或者老年代 G1 进行垃圾收集的范围是整个堆内存(包括新生代和老年代)
它采用 “ 化整为零 ” 的思路 把整个堆内存划分为多个大小相等的独立区域(Region)
在 G1 收集器中还保留着新生代和老年代的概念 它们分别都是一部分 Region 如下图:
5E)13QOFE}F55)1`(2%YDL0.jpg
每一个方块就是一个区域 每个区域可能是 Eden、Survivor、老年代 每种区域的数量也不一定
JVM 启动时会自动设置每个区域的大小(1M ~ 32M,必须是 2 的次幂)最多可以设置 2048 个区域(即支持的最大堆内存为 32M*2048 = 64G)
假如设置 -Xmx8g -Xms8g 则每个区域大小为 8g/2048=4M

为了在 GC Roots Tracing 的时候避免扫描全堆
在每个 Region 中都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系
在前面的几款分代收集中 新生代、老年代中也有一个 Remembered Set 来实时记录与其他区域的引用关系
在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除 而不用扫描全堆的数据

G1 收集器可以 “ 建立可预测的停顿时间模型 ”
它维护了一个列表用于记录每个 Region 回收的价值大小(回收后获得的空间大小以及回收所需时间的经验值)
这样可以保证 G1 收集器在有限的时间内可以获得最大的回收效率

如下图所示 G1 收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收 和 CMS 收集器前几步的收集过程很相似:
![UQ}[GDA8(]L%XWOKRCFBT6.jpg
① 初始标记:标记出 GC Roots 直接关联的对象 这个阶段速度较快 需要停止用户线程 单线程执行
② 并发标记:从 GC Root 开始对堆中的对象进行可达新分析 找出存活对象 这个阶段耗时较长 但可以和用户线程并发执行
③ 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录
④ 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序 根据用户所期望的 GC 停顿时间来指定回收计划
(用最少的时间来回收包含垃圾最多的区域 这就是 Garbage First 的由来——第一时间清理垃圾最多的区块)
这里为了提高回收效率 并没有采用和用户线程并发执行的方式 而是停顿用户线程

适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用
可以用 -XX:+UseG1GC 使用 G1 收集器(jdk9 默认使用 G1 收集器)