JVM垃圾回收机制
1 对象已死吗
垃圾收集器在对堆进行回收前,第一件事情就是要确定哪些对象是可被回收的(不能再被任何途径使用的对象)
1.1 引用计数算法
给对象中添加一个计数器,每当有一个地方引用它,计数器就+1,当引用失效时,计时器-1,任何时刻计数器为0的对象就是不可能再被使用的
优点:实现简单,判定效率高
缺点:(ObjA.instance = ObjB ObjB.instance = ObjA)对象相互引用时,若此时没有其他引用时,这两个对象是无法再被访问的,但是无法通知GC收集器回收他们
1.2 可达性分析算法
在主流的程序语言中(Java,C#)都是通过可达性分析来判断对象是否存活的
算法的基本思想是:通过一系列被称为“GC Roots”的对象作为起始节点集,从这些节点根据引用关系往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,如图所示,Object5,Object6,Object7,虽然相互关联,但是他们到GC Roots是不可达的,所以它们是可回收对象。
在Java语言中,可作为GC Roots的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般说的Native方法)引用的对象
- 虚拟机内部的引用:基本数据类型对应的Class对象,常驻的异常对象,类加载器
- synchronized持有的对象
- 反映虚拟机内部的JMXBean,JVMTI中注册的回调等
2 引用
无论是引用计数算法还是可达性分析算法,判断对象时候存活都是根据引用来判断,jdk1.2之后,Java将引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)4种
2.1 强引用
指在程序中类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
2.2 软引用
用来描述一些还有用但并非必须的对象,Java提供SoftReference类来实现弱引用
在虚拟机内存充足时,不会回收软引用关联的对象,内存不足时才会回收它
/**
* -Xms10m -Xmx10m -XX:+PrintGC
*/
public class TestSoftReference {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("1", "a");
SoftReference<HashMap> softReference = new SoftReference<HashMap>(map);
map = null;
System.out.println(softReference.get());
System.gc();
System.out.println(softReference.get());
List<byte[]> list = new LinkedList<byte[]>();
try {
for (int i = 0; i < 10; i++) {
list.add(new byte[1024 * 1024]);
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(softReference.get());
}
}
}
执行结果
2.3 弱引用
也是描述非必须对象,但是它的强度比软引用更弱一些。JDK1.2之后,提供了WeakReference类来实现弱引用
当垃圾收集器工作时,无论内存是否足够,都会回收只被弱引用关联的对象。
public class TestWeakReference {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("1", "a");
WeakReference<HashMap> weakReference = new WeakReference<HashMap>(map);
map = null;
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
}
}
执行结果:
2.4 虚引用
也被称为幽灵引用或者幻影引用,是最弱的的一种引用关系,一个对象是否有虚引用,不会对其的存活产生任何影响,为一个对象设置虚引用唯一的目的是,在这个对象被垃圾收集器回收时收到一个系统通知。
3 垃圾回收算法
3.1 标记-清除算法
标记清除算法是最基础的收集算法,分为两个阶段
- 标记:标记出所需要回收的对象
- 清除:标记完成后同一回收所有被标记的对象
标记-清除算法的执行过程如下图所示:
缺点:
- 效率不高:标记和清除两个阶段的效率都不高
- 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片过多会导致后面程序运行过程产生的大对象无法分配内存而导致提前触发垃圾回收动作.
3.2 复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可
复制算法的执行过程如下图所示:
优点:实现简单,运行高效
缺点:内存利用率低,只有一半
虚拟机采用复制算法回收新生代
IBM公司研究表明,新生代中的对象98%都是朝生夕死的,所以不需要按照1:1划分内存空间,而是将内存划分为较大的Eden区和两块较小的Survivor区,HotSpot VM默认Eden区与Survivor区的比例是8:1,也就是说只有10%的内存空间被浪费掉了
3.3 标记-整理算法
标记过程与标记-清除算法一样,后续步骤不是直接对可回收对象进行清理,而是让所有存回的对象向一端移动,然后清除端边界以外的内存。
执行过程如下图所示:
3.4 分代收集理论
- 弱分代假说:绝大多数对象都是朝生夕死的
- 强分代假说:熬过越多次垃圾收集过程的对象越难以消亡
根据不同的存储区域选择不同的垃圾回收算法
4 垃圾收集器
- 整堆收集(Full GC):收集整个堆和方法区的垃圾收集
- 混合收集(Mixed GC):收集整个新生代及部分老年代的垃圾收集,目前只有G1收集器有这种行为
- 老年代收集(Major GC/Old GC):老年代的收集,目前只有CMS收集器会有单独收集老年代的行为
- 新生代收集(Minor GC/Young GC):新生代的垃圾收集
- 并行:垃圾收集的多线程同时进行
- 并发:垃圾收集的多线程与应用程序的多线程同时进行
收集器 | 收集对象 | 算法 | 收集器类型 |
---|---|---|---|
Serial | 新生代 | 复制算法 | 单线程 |
ParNew | 新生代 | 复制算法 | 并行的多线程收集器 |
Parallel Scavenge | 新生代 | 复制算法 | 并行的多线程收集器 |
Serial Old | 老年代 | 标记整理算法 | 单线程 |
Parallel Old | 老年代 | 标记整理算法 | 并行的多线程收集器 |
CMS | 老年代 | 标记清除算法 | 并行与并发的多线程收集器 |
Garbage First | 跨老年代与新生代 | 化整为零+标记整理 | 并行与并发的多线程收集器 |
4.1 Serial/Serial Old
收集器 | 收集对象 | 算法 | 收集器类型 |
---|---|---|---|
Serial | 新生代 | 复制算法 | 单线程 |
Serial Old | 老年代 | 标记整理算法 | 单线程 |
4.2 ParNew
和Serial的区别:多线程,多CPU的,停顿时间比Serial少
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
4.3 Parallel Scavenge/Parallel Old
收集器 | 收集对象 | 算法 | 收集器类型 |
---|---|---|---|
Parallel Scavenge | 新生代 | 复制算法 | 并行的多线程收集器 |
Parallel Old | 老年代 | 标记整理算法 | 并行的多线程收集器 |
关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那有吞吐效率就是99%。
4.4 CMS收集器
收集器 | 收集对象 | 算法 | 收集器类型 |
---|---|---|---|
CMS | 老年代 | 标记清除算法 | 并行与并发的多线程收集器 |
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用集中在互联网网站或基于浏览器的B/S系统的服务端上,这类应用通常较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户最好的交互体验,CMS收集器就非常符合这类应用的需求
CMS是基于标记-清除算法实现的,
4.4.1 运作过程
分为4个步骤
- 初始标记
标记一下GC Roots能直接关联的对象,速度很快 - 并发标记
从GC Roots直接关联的对象遍历整个对象图的过程,耗时较长但是不会停顿用户线程 - 重新标记
修正并发标记期间,用户程序运作导致标记产生变动的那一部分,这个阶段停顿的时间比初始标记停顿的时间要长一点 - 并发清除
清理删除掉标记阶段判断已经死亡的对象,由于不需要移动存活的对象,这个阶段也可以与用户线程并发执行
4.4.2 优缺点
优点:并发收集,低停顿
缺点:
- CMS收集器对处理器资源非常敏感
- 无法处理浮动垃圾,有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生
浮动垃圾是指在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉 - 产生大量的空间碎片
4.5 Garbage First收集器
面向服务端应用的垃圾收集器,期望就是替换掉CMS。JDK1.9中,取代Parallel Scavenge/Parallel Old成为服务端模式下的默认垃圾收集器,CMS声明为不推荐的收集器
4.5.1 Mixed GC模式
G1之前的其他收集器,垃圾收集的目标是新生代(Minor GC),老年代(Old GC),或者整个堆(Full GC)。G1面向堆任何部分组成回收集进行回收,衡量标准不是不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收效益最大,这就是G1的混合模式(Mixed GC)
4.5.2 新的堆内存布局
G1开创了基于Region的堆内存布局,G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden,Survivor区或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。
Region中还有一种特殊的Humongous区域,专门存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代
的一部分来进行看待
4.5.3 停顿预测模型
停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的都是Region大小的整数倍,这样既可以有计划的避免在整个Java堆中进行全区域的垃圾收集,更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的价值大小,价值即回收所获得的空间大小及回收所需时间的经验值,然后再后台维护一个优先级表,每次根据用户设定的允许停顿的时间,优先处理回收价值最大的哪些region,这也是“Garbage First”的由来,这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间里内获取尽可能高的收集效率
4.5.4 运作过程
4.5.4.1 初始标记
标记GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确的在可用的Region中分配新对象,这个阶段需要停顿线程,但耗时很短。而且是借用Minor GC的时候同步完成的,所以G1收集器在这个阶段实际没有额外的停顿
4.5.4.2 并发标记
从GC Roots开始对堆中的对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行,当对象图扫描完成以后,还要重新处理SATB(原始快照)记录下的在并发时引用变动的对象
4.5.4.3 最终标记
对用户线程做一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的少量的SATB(原始快照)记录
4.5.4.4 筛选回收
负责更新Region的统计数据,对各个Region的回收价值和成本排序,根据用户所期望的停顿时间制定回收计划,可以自由的选择任意数量的Region组成回收集,然后把决定回收的Region区域的存活对象复制到空的region中,然后清理掉整个旧的Region的全部空间,涉及到存活对象的移动,必须暂停用户线程,由多条收集线程并行执行
4.5.5 主要参数
参数 | 含义 |
---|---|
-XX:G1HeapRegionSize=n | 设置Region大小,并非最终值 |
-XX:MaxGCPauseMillis | 设置G1收集过程目标时间,默认值200ms,不是硬性条件 |
-XX:G1NewSizePercent | 新生代最小值,默认值5% |
-XX:G1MaxNewSizePercent | 新生代最大值,默认值60% |
-XX:ParallelGCThreads | STW期间,并行GC线程数 |
-XX:ConcGCThreads=n | 并发标记阶段,并行执行的线程数 |
-XX:InitiatingHeapOccupancyPercent | 设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous |
5 垃圾回收器的重要参数
参数 | 含义 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收 |
UseParallelOldGC | 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收 |
SurvivorRatio | 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表 Eden : Survivor = 8 : 1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加1,当超过这个参数值时就进入老年代 |
UseAdaptiveSizePolicy | 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC 时间占总时间的比率,默认值为99,即允许 1% 的GC时间,仅在使用 Parallel Scavenge 收集器生效 |
MaxGCPauseMillis | 设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效 |
CMSInitiatingOccupancyFraction | 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效 |
UseCMSCompactAtFullCollection | 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效 |
CMSFullGCsBeforeCompaction | 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效 |