虚拟机内存管理之内存分配器

小编:本文由 WebInfra 团队姚忠孝、杨文明、张地编写。意在通过深入剖析常用的内存分配器的关键实现,以理解虚拟机动态内存管理的设计哲学,并为实现虚拟机高效的内存管理提供指引。

在现代计算机体系结构中,内存是系统核心资源之一。虚拟机( VM )作为运行程序的抽象"计算机",内存管理是其不可或缺的能力,其中主要包括如内存分配、垃圾回收等,而其中内存分配器又是决定"计算机"内存模型,以及高效内存分配和回收的关键组件。
在这里插入图片描述
如上图所示,各种编程都提供了动态分配内存对象的能力,例如创建浏览器 Dom 对象,创建 Javascript 的内存数组对象( Array Buffer 等),以及面向系统编程的 C / C++ 中的动态分配的内存等。在应用开发者角度看,通过语言或者库提供的动态内存管理(分配,释放)的接口就是实现对象内存的分配和回收,而不需要关心底层的具体实现,例如,所分配对象的实际内存大小,对象在内存中的位置排布(对象地址),可以用于分配的内存区域,对象何时被回收,如何处理多线程情况下的内存分配等等;而对于虚拟机或者 Runtime 的开发者来说,高效的内存分配和回收管理是其核心任务之一,并且内存分配器的主要目标包括如下几方面:

1. 减少内存碎片,包括内部碎片和外部碎片

  • 内部碎片:分配出去的但没有使用到的内存,比如需要 32 字节,分配了 40 字节,多余的 8 字节就是内部碎片。
  • 外部碎片:大小不合适导致无法分配出去的内存,比如一直申请 16 字节的内存,但是内存分配器中保存着部分 8 字节的内存,一直分配不出去。

2. 提高性能

内存分配需要通过内核分配虚拟地址空间和物理内存,频繁的系统调用和多线程竞争显著的降低了内存分配的性能。内存分配器通过接管内存的分配和回收,无论在单线程还是多线程场景下都可以很好的起到提升性能的作用。

3. 提高安全性

随着系统和应用对安全性的关注度提升,默认的内存分配机制无论在内存数据布局,还是对硬件安全机制的使用上都无法最大化的满足应用诉求,因此自定义内存分配器可以针对特定的系统和应用充分的利用硬件安全机制( MTE )等,同时也能够在实际系统中引入内存安全性检测机制来进一步提升安全性。

  • 检测线性溢出, UAF 等多种内存非法访问异常
  • 内存加固(地址随机化, MTE , etc .)

因此,本文通过深入分析常用的内存分配器的关键实现( dlmalloc , jemalloc , scudo , partition-alloc ),来更好的理解背后的设计考量和设计哲学,以为我们实现高效,轻量的运行时和虚拟机内存分配器,内存管理模型提供指引。

1. dlmalloc [1]

* Key Points ( salient points )

  • 从操作系统分配( sbrk , mmap )一大块内存 " Segment " ( N * page_size ),多个 Segment 通过链表相互连接," Segment "可以为不同大小。
  • 每次申请内存,从 Segment 中分配一块内存" chunk “的内存地址(如果当前 Segment 不满足分配需求,进入步骤 a. 从操作系统分配 Segment ),” chunk "可以为不同大小。
  • dlmalloc 中切分出的各中大小的 chunk 需要通过" bin “来统一管理,” Bin “用于记录最近释放的可重复使用的块(缓存)。有两种类型的 Bin :” small bin “和“ tee bin ”。Small bins 用于小于 0x100 字节的块。每个 small bin 都包含相同大小的块。” tree bin “用于较大的块,并包含给定大小范围的块。” small bin “被实现为简单的双向链表,” tree bin "被实现 chunk 大小为 key 的 tries 树。

实际的内存分配需要按照所申请的内存大小分别处理,如下所述:

  • 小内存( Small < 0x100 bytes )

小内存通过空闲链表管理空闲内存的使用状态;下图是某一时刻可能的内存快照情况,每个内存大小( size )都可作为空闲链表数组的下标访问对应大小的空闲链表。
在这里插入图片描述
基于空闲链表维护的空闲内存状态,采用如下的分配顺序和分配策略进行实际内存分配。
在这里插入图片描述

  • 大内存( Large )

大内存不采用空闲链表数组进行管理(虚拟机地址空间方范围大,时间和空间效率均低),采用 trie-tree 作为大内存的状态维护结构,每次内存分配和释放在 trie-tree 上进行高效的搜索和插入。

大内存采用如下的分配顺序和分配策略进行实际内存分配。
在这里插入图片描述

  • 超大内存( Huge ) > 64 kb

对于超大内存,直接通过 mmap 从操作系统分配内存。

* Weaknessd

  • lmalloc 不是线程安全的,因此 bionic 已经切换到更现代的堆实现方案
  • 典型的 Buddy memory allocation 算法,这种算法能够有效减少外部碎片,但内部碎片严重。

2. jemalloc ( Android 5.0.0 ~ )

该内存管理器的主要目标是提升多线程内存使用的性能( efficiency 、 locality )和较少内存碎片(最大优势是强大的多线程分配能力)

* Key Points ( salient points )

在这里插入图片描述

  • 调用 mmap 从操作系统分配一整块大内存 " Chunk ", Chunk 为固定大小( 512k ),可以被分割成两部分:头信息区域和数据区域。数据区域被分割成若干个 Run 。
  • 在 Chunk 的数据区中一个或者多个页会被分成一个组,用于提供特定大小的内存分配,被称为" Run "(相同大小内存所在的页组),相同大小 Region 对应的 Run 属于同一个 Bin ( bucket )进行管理(如下图所示)。
  • " Run “中的内存会被切分为固定小的内存块” Region ",作为最小的内存分配单元,作为实际内存地址返回给内存分配申请。
    在这里插入图片描述

上图展示了内存分配的主要流程及数据结构。jemalloc 通过 areans 进行内存统一内存分配和回收, 由于两个arena在地址空间上几乎不存在任何联系, 就可以在无锁的状态下完成分配。同样由于空间不连续, 落到同一个 cache-line 中的几率也很小, 保证了各自独立。理想情况下每个线程由一个 arena 负责其内存的分配和回收,由于 arena 的数量有限, 因此不能保证所有线程都能独占 arena , Android 系统中默认采用两个 arena ,因此会采用负载均衡算法( round-robin )实现线程到 arena 的均衡分配并通过内部的 lock 保持同步。

多个线程可能共享同一个 arena , jemalloc 为了进一步避免分配内存时出现多线程竞争锁,因此为每个线程创建一个 " tcache ",专门用于当前线程的内存申请。每次申请释放内存时, jemalloc 会使用对应的线程从 tcache 中进行内存分配(其实就是 Thread Local 空闲链表)。

每个线程内部的内存分配按如下3种不同大小的内存请求分别处理:

  • 小对象( Small Object ) 根据 Small Object 对象大小,从 Run 中的指定 " region " 返回。

  • 大对象( Large Object

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值