垃圾回收机制
Java 中不需要我们显式的释放对象的内存,该过程是由虚拟机自行执行的。在虚拟机中有一个垃圾回收线程,它是低优先级的,在正常情况下不会执行
只有在虚拟机空闲或者当前堆内存不足的时候,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收
垃圾回收算法:复制、标记-清除、标记-整理、分代收集算法
七个垃圾收集器:Serial (sɪriəl)、SerialOld、ParNew、Parallel Scavenge(pærəlel skævɪndʒ)、Parallel Old、CMS、G1
GC 分类:
- Minor GC:频繁发生在 新生区 的 普通GC
- Full GC:较少发生在 老年区 的 全局GC
1、轻GC 只针对新生区,因为大多数对象存活率不高,所以 Minor GC 非常频繁,回收速度也较快
2、重GC 发生在老年区,经常会伴随至少一次的 Minor GC,速度比 Minor GC 慢十倍以上
垃圾的确定
判断对象是否已经无效的算法:引用计数算法,可达性分析算法
1、引用计数法:给对象添加一个引用计数器,每当有一个地方引用它,计数器就加一;当引用失效,计数器减一。任何时候计数器的值为0,则对象就是不可能在被使用的
缺点:难以解决对象之间相互循环引用的问题
2、可达性分析法:为了解决引用计数法的循环引用问题,Java 使用了可达性分析算法
使用一系列的称为 GC Roots 的对象作为起始点,这些节点向下搜索的路径称为引用链,当一个对象到 GC Roots 没有任何引用链连接,则证明对象是不可用的
可作为 GC Roots 的对象包括以下几种:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
不可达并非非死不可
在可达性分析法中,不可达的对象并非是非死不可的,这时候它们暂时处于缓刑阶段,要真正宣告一个对象死亡,至少要经历 两次标记 过程
可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法
当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收
无用类的判定
判定一个类是否是无用的类,需要满足下面三个条件:
1、该类的所有实例都已经被回收了,也就是堆中不存在该类的任何实例
2、加载该类的 ClassLoader 已经被回收
3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法
复制算法、标记-清除、标记-整理(标记-压缩)、分代收集
复制算法
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理,轻GC 采用的就是复制算法
优点:效率高,不会产生内存碎片,可以利用 bump-the-pointer 实现快速内存分配
缺点:耗空间,当对象的存活率很高时,复制算法会耗费很多时间
标记-清除
分为标记和清除阶段
1、标记出所有不需要回收的对象,在标记完成后,统一回收所有没有被标记的对象
2、回收后会判断分块与前一个空闲分块是否连续,若连续,会合并这两个分块
存在的两个问题:
(1)效率问题。两次扫描,耗时严重,效率低。在进行 GC 的时候会暂停程序
(2)空间问题。在标记清除后会产生大量不连续的碎片(会产生内存碎片),内存的布局会变得乱七八糟,会导致无法给大对象分配内存
优点:不需要额外的空间
标记-整理
根据老年代的特点提出的一种标记算法,标记过程与标记-清除一样,后续不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
缺点:效率不高,不仅要标记对象,还需要移动大量对象,处理效率比较低
优点:不会产生内存碎片
效率比较
效率:复制 > 标记清除 > 标记整理
内存整洁度:复制 = 标记整理 > 标记清除
内存利用率:标记整理 = 标记清除 > 复制
分代收集算法
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
一般将堆分为新生代和老年代
- 新生代使用:复制算法
- 老年代使用:标记 - 清除 或 标记 - 整理 算法
为什么进行分代
在新生代中,每次收集都会有大量的对象死去,所以我们可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集;而老年代中对象的存活几率是比较高的,在没有额外的空间能够对它进行分配担保的情况下,必须选择标记-清除或标记-整理算法进行垃圾收集
通过这样的分代,提高了对象内存分配和垃圾回收的效率,如果没有分代,所有新建的对象和生命周期很长的对象都放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,每次回收都要遍历所有对象,这将会花费很长的时间
GC 的执行流程
1、对象的创建一般都是在 Eden 中分配内存的,当 Eden 的内存不足时,会进行一次新生代的垃圾收集(Minor GC)
在第一次 轻GC 的垃圾清除后,将存活下来的对象拷贝到 From 中,并把对象的年龄加一
在第二次往后的 轻GC 时,会对 Eden 和 From 中的对象一起进行回收,并把存活的对象 (1)复制 到 To 中,年龄加一
当有对象的年龄达到15(默认,不同收集器值不同,如CMS为6)时,就把对象转移到 Old 中,(2)清空 Eden 和 From,(3)交换 From 和 To 的身份,保持 To 一直处于空的状态,存活的对象都保存在 From 中
在这期间,会根据 From 的内存占用率,来动态设定对象晋升的 age
按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值 age:min(age, MaxTenuringThreshold)
2、如果在进行一次 轻GC 后,From 中没有足够的空间来保存对象,则需要依赖老年区,通过 分配担保机制 进行分配担保,将这些存活的对象保存到老年区中
3、如果老年区也满了,则会进行一次 重GC(Full GC)
4、当对老年区进行 重GC 后,如果三个空间都满了,则报出 OutOfMemoryError 异常
分配策略:
1、对象优先在 Eden 分配
2、大对象直接进入老年代
3、长期存活的对象将进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)
问题:为什么大对象直接进入老年区
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率
空间分配担保
空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间
1、 JDK 6 Update 24 之前,Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
- 如果这个条件成立,那这一次 Minor GC 可以确保是安全的
- 如果不成立,则虚拟机会先查看 -XX:HandlePromotionFailure 参数的设置值是否允许担保失败。如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 -XX: HandlePromotionFailure 设置不允许冒险,那这时就要改为进行一次 Full GC
2、JDK 6 Update 24 :只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC
内存比例分配
新生代中,Eden 区和 From、To 的比例是:8:1:1,该比例能充分利用内存空间,减少内存的浪费
新生代与老年代的内存比例为 1:2
对象晋级老年区
对象晋级 Old 的情况:
- 对象经历了 15 次 GC
- 大的对象直接在 Old 中创建
- Survivor 内存不足时,对象可能直接晋级到老年代
GC 触发条件
- Partial GC:并不收集整个 GC 堆的模式
- Full GC(Major GC):收集整个堆,包括 young、old、perm
以 serial 的 GC 来看:
1、young GC:当 Eden 分配满的时候触发,young GC 中有部分存活对象会晋升到 old,所以 young GC 后 old 的占用量通常会有所升高
2、full GC:当准备触发一次 young GC 的时候,如果发现统计数据说之前 young GC 的平均晋升大小比目前 old 剩余的空间大,则不会触发 young GC 而转为触发 full GC
在 HotSpot VM 中,除了 CMS 的 concurrent collection 外,其他能收集 old 的 GC 都会同时收集整个堆,包括 young,所以不需要事先触发一次单独的 young GC
如果有 perm gen 的话,要在 perm 分配空间但已经没有足够空间时,也要触发一次 full GC
或者 System.gc()、heapdump 带GC,默认也是触发 full GC
例外情况1
在 Parallel Scavenge 中,在触发 full GC 前先执行一次 young GC,并且两次 GC 之间能让应用程序稍微运行一小下,以期降低 full GC 的暂停时间(young GC 会尽量清理了 young 的死对象,减少了 full GC 的工作量)
例外情况2
对于并发 GC 的触发条件:以 CMS 为例,它主要是定时去检查 old gen 的使用量,当使用量超过了触发比例就会启动一次 CMS GC,对 old 做并发收集
垃圾收集器
根据分代收集算法,针对不同的类型,不同的分区,采用不同的垃圾收集器
- Serial:单线程下最高效
- Serial Old:Serial 的老年代版本
- ParNew:Serial 的多线程版本,与 CMS 配合使用
- Paraller Scavenge(pærəlel skævɪndʒ):多线程收集器,以吞吐量优先
- Parallel Old:Parallel Scavange 的老年代版本
- CMS:标记-清除,老年代、低延迟收集器。以获取最短回收停顿时间为目标,对 CPU 资源非常敏感,并且无法处理浮动垃圾
- G1:标记-整理,可预测停顿,把内存化整为零,碎片空间较小,可以直接对新生代和老年代一起回收
并发:用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上
并行 (Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行
Serial
串行收集器,是一个单线程的收集器
该收集器只会使用一条垃圾收集线程去完成垃圾收集工作,在进行垃圾收集时,会暂停其他所有的工作线程,直到它收集结束
优点:简单而高效(没有线程交互的开销)
新生代采用标记-复制算法,老年代采用标记-整理算法
Serial Old
Serial 收集器的老年代版本,它同样是一个单线程收集器
主要用途:
- 在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用
- 作为 CMS 收集器的后备方案
ParNew
ParNew 收集器是 Serial 的多线程版本
除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样
新生代采用标记-复制算法,老年代采用标记-整理算法
它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 配合工作
Parallel Scavenge
使用 标记-复制 的多线程收集器
关注点是 吞吐量(CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值),CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间
Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择
新生代采用标记-复制算法,老年代采用标记-整理算法。
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能
-XX:+UseParallelGC
使用 Parallel 收集器+ 老年代串行
-XX:+UseParallelOldGC
使用 Parallel 收集器+ 老年代并行
Parallel Old
Parallel Scavenge 收集器的老年代版本
使用多线程和 标记-整理 算法。在注重 吞吐量以及 CPU 资源 的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器
CMS
CMS(Concurrent Mark Sweep):一种以获取 最短回收停顿时间 为目标的收集器。符合在注重用户体验的应用上使用
CMS 是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
使用 标记-清除 算法,整个运作过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫
主要优点:并发收集、低停顿
三个缺点:
- 对 CPU 资源敏感
- 无法处理浮动垃圾
- 标记-清除 会导致大量空间碎片产生
G1
G1 (Garbage-First) :面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征
特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU 缩短停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念
- 空间整合:基于 标记-整理 实现的收集器;从局部上来看是基于 标记-复制 算法实现的
- 可预测的停顿:降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内
G1 收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
ZGC
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
在 ZGC 中出现 Stop The World 的情况会更少
G1 与 CMS 的区别
- CMS 是基于 标记-清除 实现的,G1 是基于 标记-整理 实现的
- CMS 对 CPU 的要求比较高,G1 将内存划分了很多块,所以对内存段的大小有很大的要求
- CMS 会有很多内存碎片,G1 的碎片空间较小
- G1 和 CMS 都是响应优先,目的都是尽量控制 GC 的停顿时间
吞吐量优先的话可以选择 Parallel Scavenge 多线程收集器