- TLDR:最初的Kotlin/Native内存管理方法非常容易实现,但它给试图在不同平台之间共享Kotlin代码的开发者带来了一系列问题。
早在2020年,官方就发布了重新设计Kotlin/Native中内存管理方法的计划。最初的博文描述了历史背景,列出了现在所面临的问题,并解释了为什么必须改变方法。
现在官方对进展进行了更新,并分享了一些关于内存管理设计的细节。此外,他们还计划在2021年夏季结束前提出一个开发预览。
自动内存管理的基础知识
自动内存管理的概念和编程语言一样古老。早期的编程语言根本就没有堆的概念。所有的内存都是静态或堆栈分配的。开发人员不必在运行时考虑内存的分配和删除,因为内存总是被分配(静态)或自动管理(通过堆栈),没有什么需要 "手动 "完成。这使事情变得简单易行,但它限制了软件对固定大小的数据的处理,而且对处理项目数量的限制必须在源代码中硬编码。
1966年的PL/I和Algol-68是最早的两种主要语言,它们增加了我们今天所知道的动态分配内存的概念,建立了手动内存管理的传统。这使得开发人员不得不编写显式代码来分配和释放堆中的内存。
然而,LISP的出现甚至更早。在LISP中,你可以处理动态大小的数据结构,而不必预先声明它们的大小限制,也不必写代码来分配和释放基础内存。这些都是由垃圾收集器自动完成的。作为自动化管理的一种形式,垃圾收集的概念是由John McCarthy在1959年为LISP发明的。
从那时起,无数的垃圾收集算法被创建并在各种系统中实现。这些算法的目标是自动寻找和回收垃圾运行中的程序不再需要的内存区域。
算法在识别垃圾的方式上有所不同,一般分为两大类。
- 引用计数的 垃圾收集算法从对象图的叶子开始,跟踪对特定内存区域的引用数量。一个仍然被程序使用的活的对象,将有一个非零的引用计数。
- 追踪 垃圾收集算法从对象图的根部开始,遍历该图以识别所有活的对象。一个不被程序使用的死对象,将不会被触及。
垃圾收集的科学有一整套算法,其中许多是这两种方法的混合体。如果你想了解更多,一个好的开始是《垃圾收集手册》 ,以及 Richard Jones等人撰写的《垃圾收集手册:自动内存管理的艺术》。
参考计数和跟踪垃圾收集
一个常见的误解是,垃圾收集与引用计数完全不同。事实上,引用计数是一个广泛的垃圾收集算法家族的公正名称。在这种情况下,垃圾收集往往更具体地指的是跟踪式垃圾收集。
另一个流行的误解是,引用计数的垃圾收集器一旦不再需要,就会回收空闲内存。虽然有可能实现一个具有这种属性的引用计数垃圾收集器,但很少这样做,因为立即引用计数会给引用重的代码增加相当大的开销。
选择一个垃圾收集器算法涉及到在垃圾收集器的吞吐量(它在每个分配或回收对象的数量上的开销)和内存回收的及时性(或它需要多少更多的内存空间来操作)之间找到正确的平衡。实用的引用计数垃圾收集实现通常使用延迟计数和其他类似的技术来提高运行时性能,同时在这个过程中牺牲了内存的使用。
Kotlin/Native垃圾收集器
最初的Kotlin/Native自动内存管理器使用了一个递延引用计数的垃圾收集器。选择它是因为它的简单性--而不是因为它的内存消耗。但这个最初的选择现在成了提高Kotlin/Native的性能和开发者体验的障碍。
还有收集暂停的问题。与另一个流行的神话相反,引用计数的垃圾收集器本质上不是无暂停的。提示性的内存分配和回收具有较低的吞吐量,并可能导致显著的分配延迟。此外,这些收集器不能轻易地转移到后台线程,以避免阻塞应用程序的关键线程。当试图通过延迟和分组去分配来减少整个引用计数的开销时,就像Kotlin/Native目前所做的那样,暂停变得更少,但时间更长。
与引用计数法相比,现代追踪垃圾收集算法比引用计数法的垃圾收集器要灵活得多,可调整得多,而且更容易适应多线程应用的需要。然而,所有追踪式垃圾收集器都有一个共同的弱点--它们需要来自编程语言运行时和编译器的相当复杂的基础设施。
新的垃圾收集器基础设施
目前,正在研究新的基础设施。第一个任务是确定根--内存中所有可以存储对动态分配内存的引用的位置。这将使得我们能够开始追踪一个对象图。根潜伏在全局变量、线程位置和调用栈中,以及与平台库的边界上。
此外,还需要一个特殊的基础设施来实现并发的垃圾收集算法。为什么这么麻烦?因为团队的主要使用场景是运行UI应用程序。UI应用程序有一个对延迟敏感的主线程,所以只支持停止世界的垃圾收集的设计对Kotlin/Native来说是不可行的。
在程序执行过程中,追踪式垃圾收集器必须知道的根的集合不断变化。所以决定使用所谓的安全点方法,即根据所有根是否存储在可预测的位置,将编译后的代码标为安全或不安全。这些位置对运行时来说是已知的,这意味着垃圾收集可以与处于安全状态的代码同时运行。
设计的另一个目标是实现与平台库的无缝互操作性,这需要增加对自动管理的内存的句柄泄露到非管理世界的跟踪支持,对弱引用的支持,以及在自动管理的Kotlin对象有一个附加的平台特定对象的情况下运行额外的去分配代码支持。
为了测试新的基础设施,团队正在编写一个简单的停止世界的标记和清扫垃圾收集器。这不是在生产中使用的算法,但它利用了任何跟踪垃圾收集器实现所依赖的全部基础设施。通过对这个实现进行大量的测试,希望能够找到底层实现中所有潜在的错误(比如被遗忘的根引用,或者没有被标记的不安全区域的代码)。第一个实现将不支持多线程应用,只用于测试。
接下来的步骤
下一步是编写一个具有多线程能力的垃圾收集实现,将kotlinx.coroutines
库移植到它上面,并进行测试。他们的目标是让你能够在达尔文上使用多线程的后台调度队列并自由地启动循环程序,而不必冻结在后台线程中工作的对象。这将成为第一个公开的里程碑,Kotlin团队计划在2021年夏末将其作为开发预览版提出。它不会是生产就绪的,因为它将专注于正确性,而不是性能。
一旦有了跟踪垃圾收集器的正确实现,将更专注于性能,并在更确定的去分配、内存使用和吞吐量之间进行各种权衡。在这一点上,Kotlin将能够使用许多不同的垃圾收集器实现,包括混合的,因为它的底层基础设施是专门为支持可插入的垃圾收集算法而设计的,包括引用计数和跟踪。
Kotlin计划实现一个生产就绪的垃圾收集实现,该实现支持线程之间无障碍的共享对象,并满足所有其他设计目标。然而,在未来,还可能会有一些支持的垃圾收集算法,这些算法都针对不同的使用情况进行了优化。
起初,将继续支持原有的Kotlin/Native内存管理方案,以简化现有Kotlin/Native应用程序的迁移。在构建你的Kotlin/Native应用程序时,你将能够选择你的垃圾收集实现。你的应用程序的源代码将不会改变,因为选择将通过编译器标志进行。