解密方舟的高性能内存回收技术——HPP GC
原文:https://mp.weixin.qq.com/s/o8uuP1XViIyviveL4m-8ZQ,点击链接查看更多技术内容。
众所周知,内存是操作系统的一项重要资源,直接影响系统性能。而在应用蓬勃发展的今天,系统中运行的应用越来越多,这让内存资源变得越来越紧张。在此背景下,方舟JS运行时在内存回收方面发力,推出了高性能内存回收技术——HPP GC(High Performance Partial Garbage Collection)。本文我们将从GC的基础入手,由浅入深地为大家介绍HPP GC。
一、什么是GC?
GC(全称Garbage Collection),字面意思是垃圾回收。在计算机领域,GC就是找到内存中的垃圾,释放和回收内存空间。目前主流编程语言实现的GC算法主要分为两大类:引用计数和对象追踪(即Tracing GC)。
1. 引用计数
我们通过一个示例来了解什么是引用计数:
当一个对象A被另一个对象B指向时,A引用计数+1;反之当该指向断开时,A引用计数-1。当A引用计数为0时,回收对象A。
● 优点:
引用计数算法设计简单,并且内存回收及时,在对象成为垃圾的第一时间就会被回收,所以没有单独的暂停业务代码(Stop The World,STW)阶段。
● 缺点:
在对对象操作的过程中额外插入了计数环节,增加了内存分配和内存赋值的开销,对程序性能必然会有影响。最致命的一点是存在循环引用问题。
function main() {
var parent = {};
var child = {};
parent.child = child;
child.parent = parent;
}
比如以上代码中,对象parent被另一个对象child持有,对象parent引用计数加1,同时child也被parent持有,对象child引用计数也加1,这就是循环引用。一直到main函数结束后,对象parent和child依然无法释放,导致内存泄漏。
2. 对象追踪
为了方便大家理解对象追踪算法,我们通过一个图来进行介绍:
如图1所示,从根对象开始遍历对象以及对象的域,所有可达的对象打上标记(黑色),即为活对象,剩下的不可达对象(白色)即为垃圾。
图1 对象追踪
● 优点:
对象追踪算法可以解决循环引用的问题,且对内存的分配和赋值没有额外的开销。
● 缺点:
和引用计数算法相反,对象追踪算法较为复杂,且有短暂的STW阶段。此外,回收会有延迟,导致比较多的浮动垃圾。
引用计数和对象追踪算法各有优劣,但考虑到引用计数存在循环引用的致命性能问题,方舟JS运行时选择基于对象追踪(即Tracing GC)算法来设计自己的GC算法。
二、Tracing GC介绍
在介绍HPP GC之前,我们先来了解一下Tracing GC。从前面的介绍可知,Tracing GC算法通过遍历对象图标记出垃圾。而根据垃圾回收方式的不同,Tracing GC可以分为三种基本类型:标记-清扫回收、标记-复制回收、标记-整理回收。
1. 标记-清扫回收
此算法的回收方式是:完成对象图遍历后,将不可达对象内容擦除,并放入一个空闲队列,用于下次对象的再分配。
该种回收方式不需要搬移对象,所以回收效率非常高。但由于回收的对象内存地址不一定连续,所以该回收方式最大的缺点是会导致内存空间碎片化,降低内存分配效率,极端情况下甚至会出现还有大量内存的情况下分配不出一个比较大的对象的情况。
图2 标记-清扫回收
(注:灰色块表示可达对象的内存空间,白色块表示不可达对象的内存空间。)
2. 标记-复制回收
此算法的回收方式是:在对象图的遍历过程中,将找到的可达对象直接复制到一个全新的内存空间中。遍历完成后,一次将旧的内存空间全部回收。
显然,这种方式可以解决内存碎片的问题,且通过一次遍历便完成整个GC过程,效率较高。但同时在极端情况下,这种回收方式需要预留一半的内存空间,以确保所有活的对象能被拷贝,空间利用率较低。
图3 标记-复制回收
3. 标记-整理回收
此算法的回收方式是:完成对象图遍历后,将可达对象(红色)往本区域(或指定区域)的头部空闲位置复制,然后将已经完成复制的对象回收整理到空闲队列中。
这种回收方式既解决了“标记-清扫回收”引入的大量内存空间碎片的问题,又不需要像“标记-复制回收”那样浪费一半的内存空间,但是性能上开销比“标记-复制回收”稍大一些。
图4 标记-整理回收
综上所述,Tracing GC的三种基本类型各有优缺点,那么方舟JS运行时的HPP GC是基于哪种类型实现的呢?
HPP GC同时实现了这三种Tracing GC算法!没想到吧?HPP GC综合了这三种算法的优点,且支持根据不同对象区域、采取不同的回收方式。下面就为大家详细解析HPP GC。
三、HPP GC详解
前面我们提到了,HPP GC支持根据不同对象区域,采取不同的回收方式。这是基于分代模型和混合算法来实现的。另外,为了实现HPP GC(High Performance Partial Garbage Collection)中的“High Performance”(高性能),HPP GC对GC流程做了大量优化。所以下面我们就从分代模型、混合算法和GC流程优化三个方面来为大家介绍HPP GC。
1. 分代模型
方舟JS运行时采用传统的分代模型,将对象进行分类。考虑到大多数新分配的对象都会在一次GC之后被回收,而大多数经过多次GC之后依然存活的对象会继续存活,方舟JS运行时将对象划分为年轻代对象和老年代对象,并将对象分配到不同的空间。
如图5所示,方舟JS运行时将新分配的对象直接分配到年轻代(Young Generation)的From空间。经过一次GC后依然存活的对象,会进入To空间。而经过再次GC后依然存活的对象,会被复制到老年代(Old Generation)。
图5 分代模型
2. 混合算法
通过前面的介绍,我们已经知道:HPP GC同时实现了“标记-清扫回收”、“标记-复制回收”和“标记-整理回收”这三种Tracing GC算法。也就是说,HPP GC是一种“部分复制+部分整理+部分清扫”的混合算法,支持根据年轻代对象和老年代对象的不同特点,分别采取不同的回收方式。(1) 部分复制
考虑到年轻代对象生命周期较短,回收较为频繁,且年轻代对象大小有限的特点,方舟JS运行时对年轻代对象采用“标记-复制回收”算法。
(2) 部分整理+部分清扫
方舟JS运行时根据老年代对象的特点,引入启发式Collection Set(简称CSet)选择算法。此选择算法的基本原理是:在标记阶段对每个区域的存活对象进行大小统计,然后在回收阶段优先选出存活对象少、回收代价小的区域进行对象整理回收,再对剩下的区域进行清扫回收。
具体的回收策略如下:
(a) 根据设定的区域存活对象大小阈值,将满足条件的区域纳入初步的CSet队列,并根据存活率进行从低到高的排序。
(注:存活率=存活对象大小/区域大小)
(b) 根据设定的释放区域个数阈值,选出最终的CSet队列,进行整理回收。
(c) 对未被选入CSet队列的区域进行清扫回收。
由上可知,启发式CSet选择算法同时兼顾了 “标记-整理回收”和“标记-清扫回收”这两种算法的优点,既避免了内存碎片问题,也兼顾了性能。
3. GC流程优化
在内存回收时,虽然释放和回收了内存空间,让系统有了更多可用的内存资源,但内存回收过程本身需要暂停应用业务代码执行,影响用户使用应用的体验。回收内存时,如何尽可能的减小对应用性能的影响呢?
为此,我们在HPP GC流程中引入了大量的并发和并行优化,以减少对应用性能的影响。如图6所示,HPP GC流程中采用了并发+并行标记(Marking)、并发+并行清扫(Sweep)、并行复制/整理(Evacuation)、并行回改(Update)和并发清理(Clear)。
图6 HPP GC流程
(1) 并发+并行标记
在JS线程执行业务代码的同时,另外启动线程执行标记,即为“并发标记”。如果另外启动多个线程执行标记,即为“并发+并行标记”。
在并发+并行标记过程中,为确保标记的正确性和高性能,HPP GC采取了两项优化措施:
措施一:在新增引用关系时增加标记屏障(Marking Barrier),以确保标记结果的正确性。
并发标记过程中,JS线程有可能会更改对象之间的引用关系,从而导致对象标记结果出错。如图7所示,在marking线程完成对象1的标记、准备标记对象2的过程中,JS线程更改了对象3的引用关系。那么marking线程完成对象2的标记后,对象3不会被标记,回收器会判定对象3为垃圾,进行回收。此后,如果JS线程读取对象1的字段,将会发生不可预知的错误。
图7 对象标记出错
为确保标记结果的正确性,HPP GC在新增引用关系时增加标记屏障。如图8所示,在marking线程完成对象1的标记、准备标记对象2的过程中,JS线程更改了对象3的引用关系。此时,JS线程会将对象3加入等待标记队列,等marking线程完成对象2的标记后,继续对象3的标记,从而确保对象3不会被回收。
图8 增加标记屏障
措施二:在共享全局工作队列的基础上,增加了本地工作队列(Local Work List),以提高读取对象的性能。
如图9所示,并行标记时,每个Marking线程都要执行以下操作:从全局工作队列(Global Work List)获取一个对象,然后设置标记位,最后遍历该对象的每个域,将子对象放入全局工作队列中。考虑到线程之间读取数据安全,必须在每个对象的Push/Pop操作时增加原子化或者锁,这对对象的读取性能有较大的影响。
图9 全局工作队列
为提高读取对象的性能,HPP GC增加了本地工作队列。每个线程持有一个独立的本地工作队列,优先从本地工作队列获取/放入对象。当本地工作队列满时,将本地工作队列的部分队列一次发布到全局工作队列中;当本地工作队列空时,一次从全局工作队列获取若干对象到本地工作队列。这样,只有从全局工作队列发布/获取对象那一次需要增加原子化或者锁,兼顾了多线程的并发效率和任务均衡,大大提高了并发标记的效率。
图10 增加本地工作队列
(2) 并发+并行清扫
在JS线程执行业务代码的同时,另外启动线程执行清扫,即为“并发清扫”。如果另外启动多个线程执行清扫,即为“并发+并行清扫”。
在并发+并行清扫过程中,HPP GC采取增加区域空闲队列(Region Free List)的优化措施,用于提高多线程并发清扫的效率。
并发+并行清扫过程中,Sweeping线程发现不可达对象后,需要将对象放入全局的空闲队列,同时JS线程执行的业务代码可能需要从空闲队列分配对象。为了确保数据安全,这个过程需要增加原子化或者锁,但会影响到分配和清扫的效率。
为了提升效率,HPP GC增加区域空闲队列。将所有需要清扫的内存按内存地址分成若干个区域,每个区域拥有独立的空闲队列,且每个区域同时只有一个线程进行清扫。在并发清扫过程中,Sweeping线程会首先将不可达对象放入本区域的空闲队列。当JS线程需要从空闲队列分配对象时,先获取已经完成清扫的区域,再将这些区域的空闲队列发布到全局空闲队列中,用于对象分配,如图11所示。由于发布的任务由JS线程单独完成,所以整个并行清扫的过程都不需要加锁,大大提高了并发+并行清扫的效率。
图11 增加区域空闲队列
(3) 并行复制/整理
在JS线程暂停业务代码(STW)对可达对象进行复制/整理时,另外启动多个线程一起进行复制/整理,即为“并行复制/整理”。
和并发+并行清扫相似,并行复制/整理的瓶颈点在于多个线程同时从全局空闲队列/全局线性分配器分配对象时,需要增加原子化或者锁。为了提高多线程分配性能,在并行复制/整理过程中引入了TLAB Allocator。
TLAB英文全称为Thread Local Allocation Buffer。顾名思义,TLAB Allocator就是每个线程拥有一个独立的本地分配器,该分配器会从全局内存分配器一次分配一块较大的内存,然后在线程内部再分配。这样只需从全局分配器分配时保证多线程安全,即可完成高性能且安全的并行复制/整理功能。
(4) 并行回改
在GC完成标记和复制/整理后,需要将可达对象中指向旧对象地址的域改成新对象地址,这个过程就是“地址回改”,如图12所示。为了提升地址回改的效率,HPP GC引入了“并行回改”,同时启动多个线程进行地址回改,每个线程负责其中一块内存区域对象地址的回改。
图12 地址回改
(5) 并发清理
在GC复制/整理结束后,JS线程将不再读写遍历出来的不可达对象和已经完成复制的可达对象,因此需要清理和回收对应的物理内存。为了减少STW时间,HPP GC引入“并发清理”,另外启动一个工作线程进行并发的物理内存回收,这样JS线程就可以继续执行业务代码。
四、结束语
本期就为大家介绍到这里了,最后我们总结一下:
● HPP GC基于分代模型将对象分为年轻代和老年代对象。
● HPP GC基于Tracing GC的三种基本类型,实现了“部分复制+部分整理+部分清扫”的混合算法,从而实现根据不同对象区域、采取不同的回收方式。
● HPP GC通过在GC流程中引入了大量的并发和并行优化,实现高性能。
HPP GC仍有很大的探索空间,我们还将继续努力,为大家提供更高性能的内存回收能力!
解密方舟的高性能内存回收技术——HPP GC的更多相关文章
- python的内存回收机制即gc模块讲解
最后容易造成内存问题的通常就是全局单例.全局缓存.长期存活的对象 引用计数(主要), 标记清除, 分代收集(辅助) 引用计数为0则会被gc回收.标记删除可以解决循环引用的问题.分代:0代--年轻代:1 ...
- lock free数据结构内存回收技术-hazard pointer
lock free数据结构一般来说拥有比基于lock实现的数据结构更高的性能,但是其实现比基于lock的实现更为复杂,需要处理的难题包括预防ABA问题,内存如何重用和回收等.通常,最简单最有效的处理A ...
- 《深入理解Java虚拟机》学习笔记之内存回收
垃圾收集(Garbage Collection,GC)并不是Java语言的半生产物,事实上GC历史远比Java久远,真正使用内存动态分配和垃圾收集技术的语言是诞生于1960年的Lisp语言.经过半个世 ...
- Btree并发内存回收
在并发写Btree原理剖析 一文中提到,节点内存回收有可能导致内存突增以及影响写性能.本文将阐述最近对内存回收的改进,多线程可并行回收内存. 回收策略 采用基于版本的机制,Btree全局维护一个版本号 ...
- Java技术专题之JVM逻辑内存回收机制研究图解版
一.引言 JVM虚拟机内存回收机曾迷惑了不少人,文本从JVM实现机制的角度揭示JVM内存回收的原理和机制. 一.Java平台逻辑架构 二.JVM物理结构 通过从JVM物理结构图我们可以看到: 1.JV ...
- 解密国内BAT等大厂前端技术体系-腾讯篇(长文建议收藏)
1 引言 为了了解当前前端的发展趋势,让我们从国内各大互联网大厂开始,了解他们的最新动态和未来规划.这是解密大厂前端技术体系的第三篇,前两篇已经讲述了阿里和百度在前端技术这几年的技术发展.这一篇从腾讯 ...
- 解密国内BAT等大厂前端技术体系-美团点评之下篇(长文建议收藏)
引言 在上篇中,我已经介绍了美团点评的业务情况.大前端的技术体系,其中大前端的技术全景图如下: 上篇重点介绍了工程化和代码质量的部分,工程化涵盖了客户端持续集成平台-MCI.全端监控平台-CAT.移动 ...
- Java基础-JVM内存回收
Sun的JVMGenerationalCollecting(垃圾回收)原理是这样的:把对象分为年青代(Young).年老代(Tenured).持久代(Perm),对不同生命周期的对象使用不同的算法.( ...
- 图片_ _图片缓存之内存缓存技术LruCache,软引用
每当碰到一些大图片的时候,我们如果不对图片进行处理就会报OOM异常,这个问题曾经让我觉得很烦恼,后来终于得到了解决,那么现在就让我和大家一起分享一下吧.这篇博文要讲的图片缓存机制,我接触到的有两钟,一 ...
- 图片缓存之内存缓存技术LruCache,软引用
每当碰到一些大图片的时候,我们如果不对图片进行处理就会报OOM异常, 这个问题曾经让我觉得很烦恼,后来终于得到了解决, 那么现在就让我和大家一起分享一下吧. 这篇博文要讲的图片缓存机制,我接触到的有两 ...
随机推荐
- 在anaconda中为jupyter安装代码自动补全或代码自动提示功能,jupyter nbextensions不显示拓展,另附格式化代码插件的安装方法
操作步骤 进入命令行环境.我使用的是conda.有两种方式进入命令行. 方法1:通过anconda navigator界面,选择environments,选择对应环境名,选择open terminal ...
- 3、dubbo核心用法
https://dubbo.apache.org/zh/docs/v2.7/user/examples/preflight-check/ 1.启动时检查 在启动时检查依赖的服务是否可用 Dubbo 缺 ...
- 牛客周赛 Round 31(A~F)
目录 A B C D E F A #include <bits/stdc++.h> #define int long long #define rep(i,a,b) for(int i = ...
- 实现一个 SEO 友好的响应式多语言官网 (Vite-SSG + Vuetify3) 我的踩坑之旅
在 2023 年的年底,我终于有时间下定决心把我的 UtilMeta 项目官网 进行翻新,主要的原因是之前的官网是用 Vue2 实现的一个 SPA 应用,对搜索引擎 SEO 很不友好,这对于介绍项目的 ...
- Zabbix6.0使用教程 (五)—zabbix从二进制包安装上篇
大家好,我是乐乐.上一期我们已经讲过从源代码安装zabbix,本期着重讲从二进制包安装zabbix. 当我们在ZABBIX官方存储库可以看到,Zabbix SIA 提供如下官方RPM和DEB包: ·R ...
- KETTLE4个工作中有用的复杂实例--2、两表数据比较,循环取数据,比较后自动同步(部门、单位数据同步)
附:Kettle实战视频教程,需要的朋友可以看看学习下哈~~ kettle实战第一讲-文件和数据库表的互相转换处理_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili kettle实战第二讲-数据库单 ...
- PBKDF2算法:保障密码安全的利器
PBKDF2算法起源: PBKDF2(Password-Based Key Derivation Function 2)算法是一种基于密码的密钥派生函数,最初由RSA实验室的密码学家提出,用于从密码中 ...
- 多线程系列(二十一) -ForkJoin使用详解
一.摘要 从 JDK 1.7 开始,引入了一种新的 Fork/Join 线程池框架,它可以把一个大任务拆成多个小任务并行执行,最后汇总执行结果. 比如当前要计算一个数组的和,最简单的办法就是用一个循环 ...
- CSS(语义化标签、多媒体标签、新表单元素、属性选择器、结构伪类选择器、伪元素选择器、盒子模型、滤镜、calc函数、过渡)
一.HTML5新特性 概述 HTML5 的新增特性主要是针对于以前的不足,增加了一些新的标签.新的表单和新的表单属性等. 这些新特性都有兼容性问题,基本是 IE9+ 以上版本的浏览器才支持,如果不考虑 ...
- 工良出品,从零设计开发 .NET 开发框架:框架源码和教程电子书
为什么要写这个教程 在毕业之后,读者写过了大量的文章和开源项目,正是坚持一边学习一边输出,所以笔者最终从一个生菜鸡进化为一个熟菜鸡. 在程序员的成长中,我们会在思路,如何学习.如何进步,比如要成长为一 ...