内存管理(五)——内存回收

前言

上一篇介绍完了虚拟内存,这篇将要给内存管理收个尾,介绍以下内存是如何回收的。

这里所要讲的内存回收,并不是虚拟内存中的页面置换(当可使用的物理空间不足时,需要把部分页换出),而是指对用户空间中的堆段和文件映射段进行回收(用户使用 malloc、mmap 等分配出去的空间),相当于操作系统层面的自动的 free()

内存泄露

Memory Leak,内存泄漏是指向系统申请分配内存,可是使用完了以后却不归还,结果系统也不能再次将它分配给需要的程序。

内存回收

内存回收指的是对用户空间中的堆段和文件映射段进行回收(用户使用 malloc、mmap 等分配出去的空间)。用户可以手动地使用 free() 等进行内存释放。当没有空闲的物理内存时,内核就会开始自动地进行回收内存工作。回收的方式主要是两种:后台内存回收和直接内存回收。

  • 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
  • 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。

如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会触发 OOM (Out of Memory)机制,根据算法选择一个占用物理内存较高的进程,然后将其杀死,释放内存资源,直到释放足够的内存。

在这里插入图片描述

虽然在编程语言层面就已经提供了垃圾回收机制(Garbage Collection),但是当程序申请内存的速度远远大于内存回收的速度,还是会发生内存不足,所以还需要操作系统实现内存回收。

可被回收的内存类型

主要有两类内存可以被回收,而且它们的回收方式也不同。

  • 文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存
  • 匿名页(Anonymous Page):这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过操作系统的 Swap 机制,把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

文件页和匿名页的回收都是基于 LRU (最近最少使用)算法的。回收内存的操作基本都会发生磁盘 I/O,如果回收内存的操作很频繁,意味着磁盘 I/O 次数会很多,会影响系统的性能。

总结

虽然编程语言中有垃圾回收机制(Garbage Collection),可以回收用户申请的内存空间,但是由于存在申请速度远大于回收速度、内存使用后未释放等情况,所以操作系统还需要实现内存回收机制来兜底。

回收的方式有后台内存回收、直接内存回收和 OOM。后台内存回收是异步的,不会阻塞进程,回收速度较慢;直接内存回收是同步的,会阻塞进程,回收速度快;如果回收的速度仍然赶不上,则会触发 OOM 机制,以占用内存从高到低依次杀死进程,直到内存足够使用为止。

番外

malloc是如何分配内存的?

知道了用户空间的堆段和文件映射段是如何被回收的,那他们是怎么被分配的呢?

在虚拟内存的用户空间分段中曾经讲过,堆段是用来存储动态分配的内存的,是由用户自行申请使用的。接下来以最常见的 C 语言中的 malloc() 为例进行说明。

malloc() 并不是系统调用,而是 C 库里的函数,用于动态分配内存。malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。

  • 如果用户分配的内存小于 128 KB,通过 brk() 系统调用从堆分配内存;
  • 如果用户分配的内存大于 128 KB,通过 mmap() 系统调用在文件映射区域分配内存;

方式一实现的方式很简单,就是通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间。

在这里插入图片描述

方式二通过 mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。

在这里插入图片描述

malloc() 分配的是虚拟内存,如果分配后的虚拟内存没有被访问的话,是不会将虚拟内存不会映射到物理内存,这样就不会占用物理内存了。只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。

free 释放:

malloc 返回给用户态的内存起始地址会比进程的堆空间起始地址多 16 字节,用于保存了该内存块的描述信息,比如有该内存块的大小。

这样当执行 free() 函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了。

在这里插入图片描述

  • malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用
  • malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放

优点:

  • 通过 brk() 方式申请内存,会预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。等下次再申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。
  • 通过mmap() 方式申请内存,释放时不会留下碎片空间。

缺点:

  • 通过 brk() 方式申请内存,释放时会留下碎片空间,可能导致内存泄露。
  • 通过mmap() 方式申请内存,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
是的,内存管理和泄漏是Unity游戏开发中的一个常见问题。在Unity中,Mono运行时负责管理内存,Mono是一种跨平台的开源实现,用于C#和其他.NET语言的运行时环境。Mono的垃圾回收机制可以自动回收不再使用的对象,但是如果开发者不小心,仍然可能会发生内存泄漏。 以下是一些常见的Mono内存管理和泄漏问题: 1. MonoBehaviour对象的生命周期:MonoBehaviour是Unity游戏中最常用的组件之一,但是如果在游戏对象被销毁之前没有正确释放对其的引用,就可能会导致内存泄漏。 2. 频繁的实例化和销毁对象:在一些场景中,需要频繁地实例化和销毁对象,而如果不及时释放对这些对象的引用,就会导致内存泄漏。 3. 资源加载和释放:资源加载和释放也是一个常见的内存管理问题。如果不及时释放对已经加载的资源的引用,就会导致内存泄漏。 为了避免这些问题,开发者可以采取以下措施: 1. 使用对象池:对象池是一种常见的优化技术,它可以避免频繁地实例化和销毁对象,从而减少内存分配和垃圾回收的开销。 2. 及时释放资源:在游戏对象或场景不再需要某些资源时,应该及时释放对这些资源的引用。 3. 避免使用静态变量:静态变量会一直存在于内存中,因此应该避免过多地使用它们。 4. 使用内存分析工具:Unity提供了一些内存分析工具,可以帮助开发者找出内存泄漏的根本原因。 总之,内存管理和泄漏是Unity游戏开发中需要注意的一个重要方面,开发者应该注意这些问题并采取相应的措施来避免它们。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值