jvm - 经典的垃圾回收器
学习java的同学,避免不了要去了解jvm(java virtual machine),因为这是java最底层的基石,掌握了jvm,能够帮助我们更好的理解java语言。当我们去谈论java和c++的时候,一个很重要的区别就是垃圾回收,C++在创建对象之后,一定要去释放,而java语言会自动将垃圾进行回收。本系列文章总结学习jvm笔记,参考的书籍是:《深入理解java虚拟机-第3版》,需要说明的是:该书主要描述的是hot-spot虚拟机,openjdk和sun/oracle Jdk都是默认使用该虚拟机。由于个人能力有限,文中难免有理解不到位的地方,还请留言指导,在此谢过。
本文主要说明的是jvm垃圾回收器,相比上一文提到的几种经典的垃圾回收算法,垃圾回收器是不同虚拟机的实战结果。垃圾回收器主要根据自己的应用场景进行选择。本文会对不同的垃圾回收器进行对比,方便我们实战中选择。为了更好的理解垃圾回收器,需要先学习几个jvm的术语。
根节点枚举
我们知道垃圾回收算法是从GC Roots集合开始找引用链的,目前无论是哪种垃圾回收器都需要在寻找根节点的时候,用户线程都全部停顿(stop the world),我们把这种寻找根节点的过程称为根节点枚举。如果jvm单纯从方法区等区域进行寻找的话,耗时较长。为此,Hotspot虚拟机的解决方案是:提供了OopMap数据结构来存储对象的偏移量和数据类型,在类加载的时候记录进去,如果是即时编译,也会在特定的位置添加栈中哪些是可以作为GC Roots,这样就不用从方法区一个不漏的找GC Roots,可以大大提高寻找的效率。
安全点和安全区域
上面提到的根节点枚举会在特定的位置添加,我们把能够添加GC roots的位置称为安全点(safePoint)。安全点一般设置在“是否具有让程序长时间执行的特征”为标准进行选取的,一般这种长时间执行的指令有方法调用,异常跳转,循环跳转等。有了安全点之后,怎么让用户线程快速跑到最近的安全点也是一个问题,两个可选的解决方案是抢占式中断和主动式中断。抢占式中断的方案是系统让系统立马停止工作,然后判断该线程是否在安全点,不在安全点的,让其跑到安全点上。而主动式中断的方案是设置一个标志位,线程主动去轮询标志位,发现在安全点,主动挂起。
上面提到的安全点是针对活跃的线程,如果某个线程在sleep状态或者blocked状态的时候,这时线程就无法响应中断请求,这种情况需要引入安全区域的概念。所谓的安全区域是指在某一段代码中,引用关系不会发生变化,因此在这个区域中,任何位置开始垃圾收集都是安全的。
记忆集与卡表
为了解决部分区域收集中存在的跨代引用问题,垃圾回收器建立了记忆集的数据结构,用来避免整个区域被扫描。记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。为了节约成本,数据集在实现的时候,仅仅只记录了部分精确到内存区域的信息,这种实现方式成为卡表。
写屏障
这里的写屏障并不是之前并发的时候提到的内存屏障中的一种。在这里写屏障主要解决的是更新卡表数据。当出现了跨代引用时,需要将卡表元素进行更新操作,而这个操作是通过写屏障实现的。写屏障技术是一种切面的思想,通过在操作前后添加相应的动作实现。目前在G1出现之前都是使用前置动作实现的。
并发的可达性分析
在之前提到,GC Roots的扫描是需要所有的用户线程暂停的,不过,由于各种优化后使得该部分时间稳定且很短,这个是可以接受的,但是从GC Roots 开始扫描其他的对象引用时,就可能会随着堆的增大而不断加长,如果这个时间用户线程也要停止工作的话,那就会造成很长的停顿时间,所以绝大部分的收集器在这个阶段都是通过并发收集的,也就是说收集线程和用户线程一起工作,这个阶段主要是对引用的图进行扫描。如果要同时工作的话,有几个问题是需要解决的:1)已经扫描死亡的对象被重新引用;2)已经扫描的存活对象被置为死亡。第二个问题其实只是造成了垃圾没清理干净,下一次清理即可。对于第一个问题如果处理的不好的话,会导致严重的问题。为了解决第二个问题,jvm有两种思路:一种是增量更新,一种是原始快照。为了说明这个方案,需要引用三色图标进行引导。
白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
增量更新的解决方案是当黑色对象插入新的指向白色对象的引用关系时,就将这个新 插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
原始快照的解决方案是当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
在介绍完相关术语之后,我们看下经典的垃圾回收器。我们先通过一张图看下Hotspot的垃圾回收器组合关系。有线相连的代表他们是可以组合使用的。
上面的图展示了哪些是新生代回收器,哪些是老年代回收器,而G1是一个跨代的回收器。下面就来介绍一下这些回收器。
Serial收集器/Serial old 收集器
从命名上就可以知道这两个是串行收集器,这里的串行收集器主要强调的是在进行垃圾回收的时候,用户线程需要全部停止工作(stop the world),采用单线程进行回收垃圾。如下是串行收集器的工作方式,左边是serial收集器,右边是serial old 收集器。
ParNew收集器/Parallel Scavenge收集器/Parallel old收集器
并行收集器是理解上来说是采用多线程的方式工作,这也是相比于之前的串行收集器一个最大的区别。
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其他的和serial收集器类似,也是采用标记-复制算法。
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。它和parNew最大的区别在于多了几个参数(停顿时间:MaxGCPauseMillis,吞吐量大小:GCTimeRatio)用于控制吞吐量。
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。下图是并行收集器的运行示意图。
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,以标记清除算法实现,是目前使用较为频繁的一种收集器。CMS是一种老年代垃圾回收器,其收集过程分为四个步骤:初始标记 、并发标记、重新标记、并发清除。在这四个过程中,其中有两个是需要暂停用户线程:初始标记和重新标记。
初始标记:标记一下GC Roots能直接关联到的对象
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
重新标记:了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
下图是cms收集器的工作示意图:
CMS是一款优秀的垃圾回收器,其并发和低停顿的优势在很多互联网项目上有很广的应用场景。不过,CMS也存在部分缺点:产生浮动垃圾(并发收集)、空间碎片(标记清除算法)、并发标记占用用户cpu时间。
G1回收器:
G1收集器面向局部收集的设计思路并且是基于Region的内存布局形式的,其设计之初是为了取代CMS。为了打破之前分代设计的理念,G1没有使用固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。G1收集器的运作过程大致可划分为以下四个步骤:初始标记、并发标记、最终标记、筛选回收。
初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,需要暂停用户线程。
并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。
最终标记:暂停用户线程,用于处理并发阶段结束后仍遗留下来的最后的记录。
筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
下图是G1收集器的工作示意图:
G1回收器作为未来的垃圾回收器的标杆,其具有以下优势:
1) 并行和并发:充分利用现代多核cpu资源来降低停顿时间
2) 分代处理:采用不同的算法去处理熬过多次回收的对象
3) 空间整合:从整体上看G1是基于标记-整理算法,但从region上看是基于标记复制算法。标记整理不会有空间碎片的问题
4) 可预测的停顿:G1使用停顿预测模型来计算时间,该模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。从而计算出回收的成本,让用户可以在吞吐量和延迟做个平衡。毫无疑问这是G1回收器的核心优点。
既然G1回收器有这么多的优势,是不是意味着CMS就完全可以被替换掉呢?实际上并不是,G1目前并不是所有方面都压倒CMS,主要表现在负载和内存占用上。内存占用上G1回收器由于卡表相对复杂,且每个region都要占用,所以导致G1占用的堆20%以上的内存,而CMS则相对简单的多;而负载上则主要是由于G1回收器实现细节比CMS要复杂的多,导致负载也很大。
本文主要介绍了经典的垃圾回收器,主要是CMS和G1回收器相对复杂,而其他的垃圾回收器相对要简单许多。G1可以说是未来的趋势,其region的思想和可预测的停顿都是具有里程碑意义的。CMS在java8 可以说是一款必备的垃圾回收器,熟悉CMS在实际的应用中有较大的意义,毕竟目前大部分公司使用的还是java8。
本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。