朱莉娅的垃圾收集
分配
Julia使用两种类型的分配器,分配请求的大小决定使用哪一个。 最多2k字节的对象在每线程空闲列表池分配器上分配,而大于2k字节的对象通过libc malloc分配。
Julia的池分配器对不同大小类上的对象进行分区,使得池分配器管理的一个内存页(在64位平台上跨越4个操作系统页)只包含相同大小类的对象。 池分配器中的每个内存页都与存储在每个线程无锁列表中的一些页元数据配对。 页面元数据包含诸如页面是否有活动对象、空闲插槽数以及该页面中包含的空闲列表中第一个和最后一个对象的偏移量等信息。 这些元数据用于优化收集阶段:例如,一个完全没有活动对象的页面可以返回到操作系统,而不需要扫描它。
虽然没有对象的页面可能会返回到操作系统,但其关联的元数据是永久分配的,并且可能比给定页面更长。 如上所述,分配页面的元数据存储在每线程无锁列表中。 但是,免费页面的元数据可能会存储到三个单独的无锁列表中,具体取决于页面是否已映射但从未访问过(页_pool_clean),或者页面是否被懒惰地扫描,并且等待被后台GC线程(page_pool_lazily_freed),或该网页是否已被修改(页_pool_freed).
Julia的池分配器遵循"分层"分配纪律。 当请求池分配器的内存页时,Julia将:
*尝试从 page_pool_lazily_freed,其中包含在最后一个stop-the-world阶段为空的页面,但尚未被并发清扫器GC线程madvised。
*如果未能从 page_pool_lazily_freed,它会尝试从 该page_pool_clean,其中包含在前一个页面分配请求上mmaped但从未访问过的页面。
*如果未能从 游泳池_页面_清洁 而从 page_pool_lazily_freed,它会尝试从 页_pool_freed,其中包含已经被并发清理器GC线程处理的页面,并且其底层虚拟地址可以回收。
*如果它在上面提到的所有尝试中都失败了,它将mmap一批页面,为自己声明一个页面,并将其余页面插入到 页_pool_清洁.
标记和代际收集
Julia的标记阶段是通过对象图上的并行迭代深度优先搜索来实现的。 Julia的收集器是不可移动的,因此无法通过对象单独驻留的内存区域来确定对象年龄信息,而是必须以某种方式编码在对象标头或边表上。 对象标头的最低两位分别用于存储在标记阶段扫描对象时设置的标记位和代集合的年龄位。
代际收集是通过粘滞位实现的:对象只被推到标记堆栈,因此跟踪,如果他们的标记位没有设置。 当对象到达最古老的一代时,它们的标记位在所谓的"快速扫描"期间不会被重置,这导致这些对象在随后的标记阶段没有被跟踪。 然而,"全扫描"导致所有对象的标记位被重置,导致所有对象在随后的标记阶段被跟踪。 对象在它们存活的每个扫描阶段都被提升到下一代。 在mutator端,字段写入通过写入屏障被拦截,该屏障将对象的地址推送到每个线程记住的集合中,如果对象在最后一代中,并且如果正在写入的字段中的对象 然后在标记阶段跟踪此记忆集中的对象。
扫地,扫地
对Julia的对象池的扫描可以分为两类:如果由池分配器管理的给定页面至少包含一个活对象,那么一个空闲列表必须通过其死对象线程化;如果一个给定页面根本不包含活对象,那么它的底层物理内存可以通过例如在Linux上使用madvise系统调用返回到操作系统。
扫荡的第一类是通过偷工减料并行化的. 对于第二类扫描,如果通过标志启用并发页面扫描 --gcthreads=X,1 我们在后台清理线程中执行madvise系统调用,与mutator线程并发。 在收集器的stop-the-world阶段,最初将不包含活动对象的池分配页面推入 pool_page_lazily_freed. 后台扫描线程随后被唤醒,并负责从 pool_page_lazily_freed,在他们身上呼唤madvise,并将它们插入 pool_page_freed. 如上所述, pool_page_lazily_freed 也与mutator线程共享。 这意味着在分配繁重的多线程工作负载上,mutator线程通常会通过直接从一个页面分配来避免分配时的页面错误(来自访问一个新的mmaped页面或访问一个madvised pool_page_lazily_freed,而后台清理线程需要减少页面数量,因为其中一些页面已经被突变者声称。
启发法
GC启发法通过更改垃圾回收之间分配间隔的大小来调整GC。
GC启发式测量一个集合后堆大小有多大,并根据以下描述的算法设置下一个集合https://dl.acm.org/doi/10.1145/3563323总之,它认为堆目标应该与活堆有一个平方根关系,并且它也应该通过GC释放对象的速度和突变体分配的速度来缩放。 启发式方法通过计算正在使用的页数和使用malloc的对象来测量堆大小。 以前我们通过计算活动对象来测量堆大小,但这并没有考虑可能导致错误决策的碎片,这也意味着我们使用线程本地信息(分配)来决定进程范围(何时GC),测量页面意味着决策是全局的。
当堆大小达到最大允许大小的80%时,GC将执行完整收集。