JVM 垃圾回收算法与ART CC回收器实现概述

418次阅读  |  发布于2年以前

前言

在作者的上一篇文章《Android R常见GC类型与问题案例》中,对Demo应用的Heap堆结构与Space类型及相对应内存分配算法做了简要的探究,同时对Android R机器运行中常见的GC类型和具体卡顿案例进行了细致的梳理,对Android系统和应用开发人员了解学习ART GC运行策略和优化具体GC类问题具有一定的借鉴参考意义。

承接上一篇文章,本文将对JVM垃圾回收和CC回收器简单介绍,因文章篇幅原因,对CC回收器的细节本文不过多展开,敬请期待作者下一篇对CC回收器具体实现分析的文章,本文重点介绍GC的任务和工程化的挑战,以及CC回收器的特点和优势。 术语

为了后面叙述方便,首先明确以下名词的含义:

一、GC的难题(挑战)

在李晓峰的《虚拟机设计与实现:以JVM为例》一书中,提出合格的回收器必须做到的三个方面:

在GC算法的业界研究和工程化应用中,重点是如何做到:

二、GC的任务

自动回收进程运行过程中不再需要的垃圾对象(也称孤立对象)。在这个任务中有几个重点:

l 垃圾对象的判定

l 垃圾回收实现方式

l 降低GC对业务的影响(对应上一条:GC的难题)

1)垃圾对象的判定垃圾对象也称为死亡或孤立对象,相反的则是存活或活跃对象。要寻找垃圾对象,首先的前提:所有对象只有两种状态,不是存活就是死亡。所有对象的合集为存活对象合集和垃圾对象合集组成,没有第三种集合。

主流的有两种算法来判定垃圾对象:

引用关系 图示

A) 引用计数法(RC:Reference counting)

引用计数法的思想是确定哪些对象是死的,在对象从存活到死亡的过程中回收内存,因对象死亡时间不定,所以引用计数类GC具有时刻回收、内存利用率高的特点。

如上图示,引用计数法类的GC会记录每个对象的引用次数,当引用次数为0时,代表此对象没有被任何对象引用,即认为是垃圾对象。引用计数类GC有以下优势:

B) 可达性分析法

可达性分析法的思想是找出哪些对象是活的,活的对象确定后,其他的都是死亡对象(垃圾对象),通过一次批处理完整的清除垃圾对象,回收垃圾对象占用的内存。因此在一次GC的垃圾对象回收前,可以认为所有对象都是存活对象。

如上图示,左边已确定的根集对象出发直接引用和间接引用到的对象都是存活对象,因为根集中的根对象是系统运行和业务逻辑必须的对象,被根对象直接或间接引用的对象当然需要保留。

确认到所有从根集对象直接和间接的对象合集(引用关系图\对象邻接图)后,其他与根对象无关联的对象则被判定为垃圾对象,可以直接进行回收。

可达性分析类GC有以下优势:

当然,可达性分析法也有弊端:

虽然引用计数法和可达性分析法的思想和实现完全不同,但两者其实是互补关系,在业界不乏一些优秀的GC回收器实现整合了两个算法的优点,来提高内存利用率和减小暂停时间。

*这篇文章分析的CC回收器使用的是可达性分析法。

2)垃圾回收的实现方式

在周志明的《深入理解Java虚拟机》一书中,展示了下面几种经典垃圾回收算法:

建立在以下三个假说之上

弱分代假说:绝大多数对象都是朝生夕死

强分代假说:熬过越多次GC过程的对象越难以死亡 跨代引用假说:

跨代引用相对于同代引用来说仅占极少数

因此,商业虚拟机中一般都会将Java Heap堆划分成不同的区域,按照年龄大小将对象聚集在一起,匹配有差异化的回收策略,以提高GC吞吐量和提高内存利用率。

特点:实现简单,GC耗时短,但会产生大量内存碎片

特点:解决内存碎片问题,有对象复制的开销,且牺牲一半可用内存

特点:不存在内存碎片和内存浪费,对象复制与引用更新开销巨大,整理阶段一般需要挂起应用线程

a. ART中各回收器的算法体现在Android 7(N)及以前的几个Android版本中,应用在前台时使用CMS(并发标记清除)回收器,在前台时重点关注内存回收速度。应用在后台时使用HSC(同构空间压缩)回收器,实质是标记-整理算法的一种实现。

在Android 8(O)之后,应用在前后台都是用CC(并发复制)回收器,应用在前台也能整理内存,减少内存碎片化。应用在后台时,可以最大程度地进行内存整理。在Android 10(Q)版本重新引入了分代回收,每个的Region可能是老年代或者新生代。

Android各版本回收器性能对比(数据源自Google I/O 19)

表中对比发现,CC回收器在内存分配速度、内存整理等方面都有全面的提升,本文后续章节将探究CC回收器的实现原理和策略。

3)降低GC对业务的影响

基于可达性分析实现的GC无法避免的是PauseTime(STW:stop the world),主流的回收器都在致力于减少PauseTime。不支持并发的回收器进行垃圾回收时,需要先暂停进程的所有线程,等待GC线程进行根集遍历(Root visit)、Tracing标记和回收后,进程才能恢复运行。如此长的PauseTime对于用户体验是非常致命的。

为了优化PauseTime,商用回收器基本都采用并发(Concurrent)实现,比如CMS\G1\CC等。

并发机制可以极大的减少PauseTime,一般只需要在枚举线程上下文的根对象才需要暂停线程,比如ART虚拟机的CC回收器只在枚举线程根对象时暂停一次。

并发也带来了实现逻辑的复杂化,需要增加多个关键数据和机制来维护并发期间的数据准确。主要是解决以下的难题:

在CC回收器实现中,采用并发机制,极大地减少了PauseTime,依据google官方数据,PauseTime平均为0.4ms,且不随堆大小而变化,只与Roots的数量有关。

CC回收器GC过程 图示

三、CC的实现原理与特点

CC回收器并无独创性的理论突破,借鉴的是开源社区成熟的GC理论和应用成果,结合Android设备和堆内存特点进行的实现应用。主要优化内存分配速度与内存碎片,核心理论突破是将整个堆分为多个同等大小的内存段,化整为零再分而治之。

先回顾下采用CC回收器的app堆结构图。 Demo app堆结构 图示

1)回收流程概述

2)PartialGC与StickyGC流程图示

3)涉及算法

整个堆划分为多个同样大小的Region(256KB),Region内分配内存使用最高效的BumpPointer指针碰撞算法,缺点是无法单独释放某个对象的内存(漂浮垃圾无法避免),只能整个释放Region占用的空间。

全局层面为标记-整理算法,两个Region间为标记-复制算法。有效地减少内存碎片。

写屏障支持,用CardTable实现RememberSet,记录跨代/特殊Space与RegionSpace间的引用。

由CardTable,转发指针,起始快照,目标空间不变(只访问to_space对象)等特性支持。

4)参考设计

借鉴G1/Shenandoah回收器思想,化整为零,分代收集。

5)基础原理

修改对象的引用型成员变量时执行一段特殊代码。CC中引用修改会将对象关联的Card标记为Dirty,用于记录自上次GC之后和GC并发期间的引用变化。

读取对象的引用型成员变量访问目标对象时执行一段特殊代码。CC中通过目标对象的monitor_成员变量关联的LockWord对象判断目标对象是否被拷贝。未拷贝则Mutator将目标对象进行拷贝并修改原对象的LockWord使其指向新对象,且将对象的引用修正为新的目标对象。

被拷贝后的from_space原对象LockWord配置转发指针,之后所有访问原对象都会自动指向to_space新对象。

6)优势

7)不足

四、总结

本文重点着力于阐述GC并发所面对的问题和对应的优化手段,结合Android ART虚拟机中CC回收器的实现原理和回收流程,简短的概述文章,希望读者看完能增加对GC的认识。GC理论发展几十年,依靠CPU算力提升和以空间换时间等经典思想,逐步地将GC对应用线程的影响一步步降低。有个说法JAVA开发者无需过多关注GC,但系统开发者和应用架构师掌握虚拟机内存管理机制是很有必要的,对于指导业务优化和实现高级功能都有助益。

作者下一篇文章将CC回收器具体代码进一步剖析,Stay Tuned!

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8