malloc分配内存

malloc()不是系统调用而是C库里的一个函数,有两种方式向os申请堆内存:

  • 通过 brk() 系统调用从堆分配内存:将堆顶指针向高地址移动,获得新内存空间
  • 通过 mmap() 系统调用在文件映射区分配内存:通过 mmap() 系统调用中「私有匿名映射」方式在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存

如果用户分配内存小于128KB就通过 brk() 申请内存,大于128KB则通过mmap() 申请

内存满了内核如何处理

1、malloc分配的是虚拟地址。如果分配后的虚拟内存没被访问的话,虚拟内存就不会映射到物理内存,这样就不会占用物理内存了;只有在访问已分配的虚拟地址空间时,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,进程从用户态切到内核态并将缺页中断交给内核缺页中断函数处理。

2、缺页中断处理函数会看是否有空闲物理内存:有就直接分配物理内存并建立虚拟内存与物理内存间的映射关系;如果没有空闲物理内存,内核就会开始内存回收

  • 后台内存回收:物理内存紧张时会唤醒内核线程来回收内存,异步不会阻塞进程执行
  • 直接内存回收:如果后台异步回收跟不上进程内存申请的速度就会开始直接回收,同步会阻塞进程执行

3、如果在直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,此时内核就会触发OOM机制:根据算法选择一个占用物理内存较高的进程将其杀死以便释放内存资源,如果物理内存依然不足,OOM Killer会继续杀死占用物理内存较高的进程,直到释放足够的内存位置

如何保护一个进程不被OOM杀掉

OOM killer会根据每个进程的内存占用情况和oom_score_adj值进行打分,得分最高进程就会被首先杀掉。可以通过调整进程的/proc/[pid]/oom_score_adj 值来降低被杀掉概率

内存回收

回收下面两种内存都是基于LRU算法优先回收不常访问的内存(维护两个双向链表)

  • 文件页:内核缓存的磁盘数据Buffer和文件数据Cache。回收干净页就直接释放内存,回收脏页就先写回磁盘再释放内存
  • 匿名页:这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,通过Linux的Swap机制,Swap会把不常访问的内存先写磁盘然后释放,给其他更需要的进程使用,再访问时重新从磁盘读入内存

解决内存回收带来的性能问题

1、调整文件页和匿名页的回收倾向:因为文件页的回收会比匿名页的回收少一些磁盘IO,所以可以将/proc/sys/vm/swappiness设置为0,在回收内存时就会更倾向于文件页的回收

2、通过尽早触发「后台内存回收」来避免应用程序进行直接内存回收

3、NUMA架构下内存回收:SMP架构指的是一种多个CPU处理器共享资源的电脑硬件架构,每个 CPU 访问内存所用时间都是相同的,随着CPU核数增多,多个CPU都通过一个总线访问内存,总线带宽压力会越来越大,同时每个CPU可用带宽会减少;可以采用NUMA架构解决:将每个CPU进行分组,每组CPU用Node表示,一个Node可能包含多个CPU。每个Node有自己的独立资源包括内存、IO。每个 Node 之间可以通过互联模块总线通信,意味着每个Node上的 CPU 都可以访问到整个系统的所有内存。NUMA 架构下当某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。虽然访问远端 Node 的内存比访问本地内存要耗时多,但相比内存回收的危害而言,访问远端 Node 的内存带来的性能影响还是比较小的

Swap机制

把一块磁盘空间或本地文件当成内存来用,包含换出和换入两个过程:

  • 换出:把进程暂时不用的内存数据存到磁盘中并释放占用的内存
  • 换入:进程再次访问这些内存时候,把它们从磁盘读到内存中来

Swap优点是应用程序实际可以使用的内存空间将远超过系统的物理内存,由于硬盘空间的价格远比内存要低,因此这种方式无疑是经济实惠的。 Swap的弊端是频繁地读写硬盘会显著降低操作系统的运行速率。

Linux 中的 Swap 机制会在内存不足内存闲置的场景下触发

free怎么知道要释放多大内存

malloc返回给用户态的内存起始地址比进程的堆空间起始地址多了16字节,这多出来的16字节就保存了该内存块的描述信息,比如内存块的大小。这样当执行free() ,会对传进来的内存地址向左偏移16字节,然后分析出当前的内存块的大小就知道要释放多大内存了

free后会将内存归还给os吗

  • 通过brk()申的内存,free时不会把内存归还给os,而是缓存在malloc的内存池中等待下次使用
  • 通过mmap()申的内存,free时会把内存还给os,此时内存得到真正释放

为什么不全用mmap分配内存

1、因为向os申请内存是要通过系统调用的,执行系统调用是要进入内核态然后再回到用户态,运行态的切换会耗时。所以申请内存操作应避免频繁的系统调用,如果都用mmap来分配内存,等于每次都要执行系统调用。

2、因为mmap分的内存每次释放时都会归还给os,于是每次mmap分配的虚拟地址都是缺页状态,然后在第一次访问该虚拟地址时就会触发缺页中断。

也就是说频繁通过mmap分配内存,不仅每次都会发生运行态切换,还会发生缺页中断(在第一次访问虚拟地址后)导致CPU消耗较大

为改进这两个问题,malloc通过brk()在堆空间申请内存。由于堆空间连续,所以直接预分配更大的内存来作为内存池,当内存释放时就缓存在内存池中。等下次申请内存时就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,能大大降低CPU的消耗

为什么不全用brk分配内存

通过brk从堆空间分配的内存不会归还给os。随着系统频繁 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片导致“内存泄露”,而这种“泄露”现象很难检测出来