- 垃圾标记
- 引用计数法
- 可达性分析(主流使用)
- GC回收算法
- 标记-清除算法(适合老年代)
- 标记-复制算法(适合年轻代)
- 标记-整理算法(适合老年代)
- “因地制宜”——分代算法
- GC回收器
- Serial 回收器
- Serial Old 回收器
- ParNew回收器
- Parallel Old回收器
- Parallel Scavenge回收器
- CMS收集器
- G1回收器
垃圾标记
在回收垃圾前,需要判断哪些是垃圾,哪些不是。
引用计数法
原理:被引用+1,未被引用-1,为0时回收。
问题:无法解决循环引用的问题
-
什么是循环引用?(环)
A 引用了 B,B 引用了 C,C 引用了 A,它们各自的引用计数都为 1。但是它们三个对象却从未被其他对象引用,(假设有1000个对象时,这三个就是垃圾;如果只有4个对象,那么另外一个就是垃圾)只有它们自身互相引用。从垃圾的判断思想来看,它们三个确实是不被其他对象引用的,但是此时它们的引用计数却不为零。
可达性分析(主流使用)
通过GC Roots作为起点遍历,判断是否被引用。
GC Roots包括:
- 虚拟机栈中引用的对象
- 方法区静态属性引用的对象
- 方法区常量引用的对象
- JNI引用的对象(Native方法)
【引用的判定】
- 强引用:抛异常也不回收
- 软引用:内存空间不够就回收
- 弱引用:发现了就回收(按线程优先级)
- 虚引用:任何时刻都会被回收
GC回收算法
分代收集本质上就是分类讨论,根据对存活对象的预判,采用效率更高的收集算法。
标记-清除算法(适合老年代)
先通过GC Roots遍历,标记不可达对象,而后清除。
问题:产生不连续的空间碎片
标记-复制算法(适合年轻代)
折半内存,将存活对象复制到另一半内存中,然后把当前内存全部回收。
问题:解决了空间碎片,但只能使用一半的内存
标记-整理算法(适合老年代)
将存活对象移动到内存的一端,然后清除边界之外的所有内存。
【既不会产生空间碎片,也不会导致内存折半】
“因地制宜”——分代算法
分代算法,就是根据 JVM 内存的不同内存区域,采用不同的垃圾回收算法。
例如对于存活对象少的新生代区域,比较适合采用复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。
而对于老年代这种存活对象多的区域,比较适合采用标记清除算法或标记整理算法,这样不需要移动太多的内存对象。
GC回收器
Serial 回收器
Serial回收器是一种单线程串行回收器,使用复制算法,在执行回收时会产生较长时间的停顿,优点是不会产生线程切换的开销
通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
Serial Old 回收器
SO回收器是一种多线程并行回收器,使用标记整理算法,适用老年代
ParNew回收器
PN回收器是一种多线程并行器,使用复制算法。
参数控制:-XX:+UseParNewGC
Parallel Old回收器
PO是一种多线程回收器,使用标记整理算法,适用老年代
Parallel Scavenge回收器
PS回收器也是一种多线程并行回收器,使用复制算法,特点是可设置吞吐量
参数:-XX:+UseParallelGC
CMS收集器
CMS使用标记清除算法,主要目的是回收时的低停顿。
过程:
- 初始标记:单线程(触发停顿),标记与GC Roots关联的对象
- 并发标记:标记不可达对象(可能产生回收对象,所以之后需要重新标记)
- 重新标记:单线程(触发停顿),纠正标记
- 并发清除:清除对象
问题:
- CPU资源消耗高
- 浮动垃圾产生(回收时产生了新的垃圾)
- 标记清除算法导致的空间碎片问题
G1回收器
特点:
- 停顿时间:G1可充分利用多核多CPU的优势减少停顿时间,并且可建立可以预测的停顿模型,可设置停顿的时间
- 支持分代回收,可使用分区策略同时兼顾老年代和年轻代
- 使用标记整理算法,不会产生空间碎片
分区思路:
为什么使用分区:针对老年代的操作需要扫描所有的老年代空间;由于连续,必须分配年轻代和老年代的地址位置
G1之前的内存都是连续的:
G1回收器的内存按区等分,分为eden,survivor,old和humongous:
G1回收器回收流程:
- 初始标记
- 并发标记
- 重新标记
- 并发回收
Minor GC 和Full GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。
Full GC 是清理整个堆空间—包括年轻代和永久代。
Full GC执行条件:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足