JVM GC 预览
总览图
3种分代假说
- 弱分代假说:90%的对象都朝生夕灭的
- 强分代假说:熬过多次垃圾回收的对象一般都是难以消亡的
- 跨代引用假说: 跨代引用相对于同代引用来说是仅占少数的
2种判断垃圾的策略
- 引用计数法:无法解决循环引用的问题
- 根可达算法:从GC Root查找对象是否可达(目前java都是用这种方式来判断对象是否存活的)
哪些可以作为GC Root对象
- 在虚拟机栈中引用的对象(本地局部变量表中对象),参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象:java类的引用类型静态变量。
- 在方法区中常量引用的对象,例如字符串常量池(String Table)里的引用
- 在本地方法栈中JNI引用的对象。
- java虚拟机内部引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NPE,OOM)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调、本地代码缓存等。
4中引用类型
- 强引用:代码中普遍存在的引用,Object o = new Object();这个就强引用,只要强引用的关系存在,垃圾回收器就不会回收该对象。
- 软引用:软引用用来描述一些还有用,但非必须的对象,只有内存不够了才会进行垃圾回收。(一般用作缓存)
- 弱引用:弱引用用来描述一些还有用非必须的对象,但它的引用强度比软引用弱一些,被弱引用的对象只能存活到下一次垃圾回收的时间。(ThreadLocal中就是使用弱引用的)
- 虚引用:它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生产时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的只是为了在这个对象被回收器回收时收到一个系统通知。(DirectByteBuffer中内存的释放就是用的虚引用)
3种通用的垃圾回收算法
标记-清除算法
- 标记:标记哪些是垃圾(后面具体的实现,一般标记哪些是活着的对象,其他的都是垃圾)
- 清除:清除垃圾
2个缺点
- 执行效率低,大部分都是需要回收的对象,需要进行大量标记和清除
- 会产生内存碎片,内存应用率低
标记-复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率低和内存碎片化的问题,把内存区域分成2块,每次只使用其中一块,这一块内存用完了,需要把活着的对象拷贝到另一块中去,然后清空原有的内存区域,这样就是可以避免内存碎片化和执行效率低 的问题,整块清理
缺点:空间浪费,一般用户年轻代的回收算法
标记-整理算法
背景:标记-复制算法对象存活率较高的时候就需要进行较多的复杂,效率会降低。更重要的是需要浪费50%的空间
标记-整理算法主要是解决了空间浪费和内存碎片化问题,使用场景一般用于老年代,老年代的对象存放率比较高,只需要回收少部分对象,也需要考虑内存碎片化问题。
标记-整理需要移动对象,就需要更新对象的引用的信息,这是一个比较负重的操作,而标记-清除就不需要移动对象,但是会产生垃圾碎片。
整理和清除: 移动对象内存回收时比较复杂,不移动时内存分配会比较复杂。
SafePoint(安全点)
当需要STW,所有线程(不包含JNI调用的线程)都需要在SafePoint上停止,防止引用关系发生改变,垃圾回收器不同阶段的切换需要到安全点
记忆集和卡表
分代收集理论的时候,为了解决对象跨分代引用所带来的问题(yong gc的时候需要遍历老年代所有的对象来判断对象是否存活,成本有点高),垃圾收集器在新生代中建立了名为记忆集的的数据结构,用已避免把整个老年代加进GC Root扫描范围。事实上并不只是 新生代、老年代之间才有的跨代引用的问题,所有涉及部分区域收集行为的垃圾收集器,例如:G1、ZGC、Shenandoash。
卡表:是记忆集的具体实现,大概原理是内存区域映射,用1Byte表示512Byte的区域,如果是该内存区域存在分代引用就标记为1,否则为0。行为有的像BitMap之类,用一小部分内存代表大部分内存行为。
写屏障(AOP操作)
这里的写屏障是对象引用更新后和更新前的操作,就是一个AOP,不是内存屏障(SS,SL,LS,LL);主要用来维护记忆集和一些额外的操作。
并发的可达性分析
要实现并发标记,就需要了解在并发过程中引用会发生改变,这就会产生一些问题了。
- 原本已经消亡的对象被标记为存活,这就是浮动垃圾,可以接受下次回收
- 应该存活的对象被标记为死亡,这就是对象消失了,这是绝对不允许发生的。
用3色理论来分析对象可达性,具体实现不太清除
产生对象的消失的原因
- 赋值器插入了一条或多条从黑色到白色对象的新引用
- 赋值器删除了 全部从灰色对象到该白色对象的直接或者间接引用
因此我们需要解决并发扫描时候的对象消失问题,只需要破坏这个2个条件的任意一个即可。
解决方案:
- 增量更新:当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系以黑色对象为根继续扫描一下 (这个时候需要STW,不能并发扫描标记),破坏的第一个条件
- 原始快照: 当灰色对象要删除指向白色对象的引用关系是,就需要把这个要删除的记录下来,等并发扫描结束之后,需要重新将这些记录过的引用关系以灰色对象为根,重新扫描一遍。简单理解,就是开始扫描开始时候的一个快照,无论发生什么变化都需要快照的的对象图一个个扫描。
垃圾收集器
Serial收集器
特点:单线程回收,所有的收集操作都需要STW,适合于内存小,单核CPU系统,新生代的垃圾回收器,一般用于客户端和小型桌面应用
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并发版本,主要是为了搭配CMS回收器的,用于对于延迟性小的应用。新生代收集器
Parallel Scavenge 收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样用标记-复制算法实现的收集器。但是他与ParNew不同的是它们两的设计目的不一样,Parallel Scavenge收集器注重于吞吐量,而ParNew注重于用户线程的停顿时间。
小总结:上面3个 都是用于新生代的收集器,都是是采用复制算法进行回收。
Serial Old 收集器
Serial Old是Serial的老年代版本,也是一个单线程收集器,使用标记-整理算法。
2种用途:
- JDK5以及之前版本中与PS收集器搭配使用
- CMS收集器失败时的后备预案。
CMS收集器
CMS是一款基于标记-清除的老年代收集器,设计目标是:并发收集、低停顿。
过程:
- 初始标记:标记GC Root对象,需要STW
- 并发标记:从GC Root对象出发标记对象图
- 重新标记:并发标记过程中解决对象消失问题的增量更新对象的重新标记,需要STW
- 并发清除:清除并发标记中已经死亡的对象,不需要移动对象,可以并发操作
缺点:
- 并发标记和并发清除阶段,如果用户线程分配的内存大于预留下来的内存就会发生并发失败,启动Serial Old进行垃圾回收,这个时候STW的时间就比较长了。
- 使用标记-清理就会有碎片化的问题,如果由于碎片化问题导致内存分配失败时,会触发提前触发Full GC,其实这个时候老年代的内存占用率并没有达到设置的阈值,只是因为碎片化问题,导致大对象分配不了,CMS可以设置一些参数可以控制多少次Full GC 进行碎片化整理,,默认为0,每次都进行整理,因为碎片化整理需要移动对象,所以不能并发,一般是在初始标记之前一起整理。
Garbage First 收集器
G1收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
过程:
- 初始标记:标记一下GC Root能直接关联到的对象,并修改TAMS的指针,让下一 阶段用户线程并发运行时,能正确地在可用的region中分配新对象。这个阶段需要停顿线程。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时比较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的并发时有引用变动的对象。
- 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
- 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成功进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定准备回收的region移动到空的Region中,在清理掉整个旧的region的全部空间。操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并发完成的。
G1使用卡表来解决多个区域之间跨区域对象引用问题,每个region区域都需要卡表,需要占用更多的内存空间。
G1使用原始快照(SATB),解决了并发标记中对象消失的问题。