一文彻底搞懂“内存管理”

原创
存储 存储软件
笔者面试过不少业务后台开发候选人,当问起内存管理的相关问题时,往往都会答出 JVM 的垃圾回收机制,并对 Serial、Parallel、CMS 等收集器如数家珍,侃侃而谈。

【51CTO.com原创稿件】笔者面试过不少业务后台开发候选人,当问起内存管理的相关问题时,往往都会答出 JVM 的垃圾回收机制,并对 Serial、Parallel、CMS 等收集器如数家珍,侃侃而谈。

[[408196]]

图片来自 包图网

然而对于应用层以下的内存管理机制却鲜有人能答出来,甚至于认为 JVM 直接管理了物理内存。

诚然,许多的高级语言如 Java、Go、Python 等已经内置了完善的自动内存管理机制,开发者可以"开箱即用"。

但如果只知其表,不知其里,在出现系统性能问题时往往手足无措,无法全面思考解决问题。

今天笔者尝试从 0 开始,用一篇文章讲明内存管理。

V1.0:直接使用物理地址

最开始的时候,计算机只允许运行一个进程,内存也只有几百 KB 大小,那时候的世界很简单也很美好,保留一部分内存空间给 OS 使用,剩下的都是这个进程的专属空间,想怎么用怎么用,如图 1-1。

但为了更高效地利用 CPU 的计算资源,OS 需要支持"同时"运行多个进程,此时内存空间按固定大小被瓜分为几块,分属于各个进程使用,如图 1-2。

由于是直接使用内存物理地址,如果这些进程都很"本分",只访问自己的空间,那么一切都还正常,但如果某个进程闯入他人的领地,胡作非为呢?可控性是个问题。

V2.0:增加抽象转换层,使用虚拟地址

当考虑到增加管控、安全校验、动态分配等问题时,增加一层抽象进行"代理"往往是一个通用的解决方案。

到 2.0,进程不再被允许直接使用物理内存空间,而是使用从 0 开始编码的虚拟地址,经由 MMU(Memory Management Unit)转换得到实际地址,然后才能到内存中获取到数据。

中间层 MMU 会检查虚拟地址的有效性和合法性,从而保证安全性。

考虑到内存空间使用的灵活性,内存按固定大小进行分页(Paging),通常是 4KB,连续的虚拟地址页(VP,Virtual Page),映射到物理地址页(PP, Physical Page)上,可以是分散的,这种灵活的设计可以提升物理内存的空间利用率,减少内存碎片。

既然有映射,自然需要存储映射关系表,即页表(Page Table),Key 值是虚拟地址页号(Virtual Page Number)。

Value 值是包含有物理地址页号(PPN,Physical Page Number)的数据结构(PTE,Page Table Entry),值得一提的是,页表不存在 MMU 里面,同样也是存在内存里。

图 2-1 简要地展示了虚拟地址到物理地址的转换过程:

为了方便说明,这里页大小设置为 16 字节(2^4,offset 占用 4bit),总的物理内存大小有 8 页(2^3,PPN 占用 3bit)即 128 字节,虚拟内存至多使用 4 个页(2^2,vpn 占用 2bit)。

MMU 将一个 6bit 的虚拟地址转化为 7bit 的物理地址,其中通过页表完成 vpn 到 PPN 的转换,而 offset 部分保持不动。

①V2.1 时间优化:增加 TLB 缓存

在计算机领域,当考虑性能提升的问题时,使用缓存是个万金油般的解决方案。

其背后主要是基于时空局限性理论(temporal/spatial locality):时间上,一个刚被访问过的数据,很可能在不久之后被再次访问;空间上,一个刚被访问过的空间 x,很可能在不久之后 x 的邻近空间也被访问。

很自然地,我们可以在 MMU 里面加入一小块缓存空间,即快表 TLB(Translation Lookasid Buffer),里面保存着最近的 vpn->PPN 映射关系。

如果缓存命中(TLB Hit),将极大地提升地址转换速度,如果缓存未命中(TLB Miss),则重新从页表中查询。

遗憾的是,空间和时间永远是一对矛盾,TLB 容量越大,访问速度也随着降低,你无法实现一个足够大的 TLB 去替换掉内存上的页表,因此当 TLB 快满时,通常会使用近似 LRU 的算法将最少被使用的单元踢除。

图 2-2 和 2-3 分别展示了 TLB 命中和未命中情况下的流程,如果命中,则只需一次物理内存访问;如果未命中,则会先到物理内存中查询 PTE,并更新至 TLB,然后再访问真正的数据地址。

②V2.2 空间优化:多级页表和交换分区

进行时间优化后,我们再来思考空间上有哪些可以优化的。我们注意到,原始的线性页表会随着虚拟内存的增大而增大。

试算一下,一个 32bit 大小的虚拟地址(2^32),分页大小为 4KB(2^12),则会有 1M 个分页 (2^20)。

假如一个映射单元 PTE 占用 4 个字节,则光存储这个进程的映射表就需要 4MB。

如果机器上同时运行了 100 个进程,那么将吃掉 400MB 大小的内存空间!这对于整个系统来说将是极大的浪费。

避免这种浪费的关键在于,并非所有的虚拟分页都需要保存其映射关系,对于还未被使用的分页群,可以只使用一个 PTE(Page Table Entry)表示,而对于连续使用的分片群,可以使用多级映射来定位。

图 2-4 展示了二级页表的寻址过程,图中一级页表的一个 PTE 可以代表 1 千个 VP。

这样对于中段大量空闲的 VP,只需使用若干个 PTE 即可表示,显著地减小了页表的总大小,对于大容量且稀疏的虚拟地址空间,可以依此类推,再增加几级页表。

为了更高效使用我们珍贵的内存空间,除了通过多级页表节流之外,我们还能通过使用部分磁盘空间,即交换分区,作为虚拟内存来达到开源的效果。

具体来说,我们提供给上层应用的虚拟内存空间是可以大于实际可用的内存空间的。

只要 OS 时不时将一些不常用的内存数据复制到交换分区然后从内存清除,就可以源源不断地提供新的内存空间。

当读取到这部分虚拟内存时,再从交换分区恢复到内存就可以了,当然了,这种操作会一定程度上降低内存的读写速度。

图 2-5 展示了增加了交换分区后的工作流程,当 OS 发现要查找的 PTE 既不在 TLB 中,也不在内存中,就会抛出一个 Page Fault 异常,OS 再异步地从交换分区中查找出 PTE 并写回内存,完成后 CPU 再发起重试就可以了。

V3.0:无招胜有招,自动管理内存

通过上述的设计,操作系统为上层应用搭建了一个安全舒适的虚拟乐园,在这个乐园里面,应用无需关注真实的内存转换、寻址等繁琐事项,只管在需要时 malloc 申请内存,不需要时 free 掉即可。

然而随着应用复杂度的快速上升,即使是自己的一亩三分地,也常常因为疏漏或者 Bug 导致申请的内存未及时释放,造成内存泄露最终导致应用崩溃。

由此以 JVM 为代表的一系列自动内存管理平台应运而生,通过定期扫描内存中的数据对象,使用引用计数法或者可达性分析,区分出数据对象是否可回收,再结合标记-清除算法、复制算法等实现内存垃圾回收。

关于垃圾回收器的具体实现业界仍在不断地更迭出新,这里不再细述。

结语

本文尝试从最基础的设计开始,逐步引入虚拟地址转换,随后进行时间和空间上的优化,最后介绍应用层的自动内存管理机制,循序渐进,希望能帮你构建出一幅内存管理的基本蓝图。

当然,基于篇幅的限制,真实的系统设计细节远比本文介绍复杂得多,会引入更多层级的缓存、映射,并基于硬件特性做更多的优化策略以提升内存使用效率。

但大道至简,理解其最核心的设计思路,再去看技术细节,相信会帮你更快地理解领悟。

作者:李腾辉

简介:Akulaku 高级开发工程师,目前负责金融借贷平台架构设计及核心建设工作,对微服务体系、JVM 虚拟机及操作系统原理机制有较深入的研究,擅长定位并解决线上疑难问题。

编辑:陶家龙

征稿:有投稿、寻求报道意向技术人请添加小编微信 gordonlonglong

【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】

 

责任编辑:武晓燕 来源: 51CTO技术栈
相关推荐

2022-06-07 10:13:22

前端沙箱对象

2020-03-18 14:00:47

MySQL分区数据库

2020-12-07 06:19:50

监控前端用户

2021-07-08 10:08:03

DvaJS前端Dva

2019-11-06 17:30:57

cookiesessionWeb

2023-04-12 08:38:44

函数参数Context

2021-08-05 06:54:05

观察者订阅设计

2022-04-11 10:56:43

线程安全

2021-01-06 13:52:19

zookeeper开源分布式

2023-11-23 06:50:08

括号

2020-12-18 09:36:01

JSONP跨域面试官

2019-12-04 13:50:07

CookieSessionToken

2023-01-27 18:55:37

Python内置函数

2021-03-22 10:05:59

netstat命令Linux

2023-09-08 08:20:46

ThreadLoca多线程工具

2023-09-15 12:00:01

API应用程序接口

2021-05-12 18:22:36

Linux 内存管理

2021-10-20 08:49:30

Vuexvue.js状态管理模式

2022-03-24 08:51:48

Redis互联网NoSQL

2021-07-21 05:24:32

EventBus3.0Android单例模式
点赞
收藏

51CTO技术栈公众号