Mar 19, 2025
10 mins read
malloc分配的是虚拟内存,而非物理内存。当这块虚拟内存被读写了的时候,CPU会访问这块虚拟内存,然后发现该虚拟内存没有被映射到物理内存,CPU就会产生缺页中断,进程从用户态 一> 内核态。
缺页中断处理函数(Page Fault Handler)会判断是否有空闲的物理内存,如果有,就直接分配物理内存,否则回收内存。
有两种回收内存的方式:后台内存回收(异步)、直接内存回收(如果后台回收跟不上进程内存申请的速度就会开始直接回收,这个过程是同步的,会阻塞进程执行)。
如果直接内存回收,仍旧不满足申请的内存大小,那么就会触发OOM(Out Of Memory)机制。
OOM机制会根据算法杀死一个占用物理内存最高的进程,如果还是无法满足,继续杀死进程,直到释放足够多的内存。
如何保护一个进程不被OOM杀掉呢?
Linux中通过oom_badness()
对每个进程打分,得分最高的进程会被杀掉。得分机制:
oom_score_adj
[-1000, 1000]我们可以通过调整校准值来防止进程被OOM杀掉。比如说设置成-1000,无论如何都不会被杀掉。但是不建议将业务程序这样设置,因为如果某个业务程序发生内存泄漏,而又无法被杀掉,那么OOM会把其他进程都杀掉了。
哪些内存会被回收?
文件页:内核缓存的文件数据(Cache)和内核缓存的磁盘数据(Buffer)都属于文件页。如果被应用程序修改过并且还没写入磁盘的数据叫做脏页。回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。
匿名页:没有磁盘这样的实际载体,比如栈和堆。这些内存不能直接释放,因为很可能会再次被访问,通过linux的Swap
机制,将不常访问的写入到磁盘中再释放内存,如果需要访问,就从磁盘中读入内存。
文件页和匿名页的回收都基于LRU算法。
回收内存除了回收干净页,都会发生磁盘 I/O的,这会影响系统性能。
可以申请虚拟内存超过物理内存吗?
在32位操作系统,进程最大只能申请3GB的虚拟内存。
在64位操作系统,进程最大可以申请128TB的虚拟内存。
所以如果想在4GB的物理内存空间上直接申请8GB的虚拟内存,在32位操作系统上会申请失败,而在64位会成功。
如果有Swap分区,即使物理内存只有4GB,进程也可以正常使用8GB的内存。
LRU预读失效怎么办?
如果应用程序想读取 0 - 3KB 范围内的数据,由于磁盘基本读写单位为 block(4KB),于是操作系统至少会读 0 - 4KB 的内容,但是由于空间局部性原理(靠近当前被访问数据的数据,在未来很大概率会被访问到),所以会把[4, 8],[8, 12]以及[12, 16]都加载到内存。
预读机制带来的好处是减少磁盘 I/O 次数,提高系统磁盘 I/O 吞吐量。
预读失效:就是这些被提前加载进来的页面没有被访问,相当于预读白做了。不会被访问的预读页却占用了 LRU 链表前排的位置,而末尾淘汰的页,可能是热点数据,这样就大大降低了缓存命中率。
想要避免预读失效,就要尽可能地保证预读的数据停留内存的时间要短,真正的热点数据尽可能久的留在内存。Linux操作系统就实现了两个LRU链表:活跃LRU链表(active_list)和非活跃LRU链表(inactive_list)。
预读页就只需要加入到 inactive list
区域的头部,当页被真正访问的时候,才将页插入 active list
的头部。
缓存污染怎么办?
虽然两个LRU链表可以解决预读失效的问题,但是还会存在缓存污染的问题。当批量读取数据时,如果数据被访问了一次就加入到LRU中,那么活跃链表中会一下子淘汰了很多热点数据,而那些被新加入的数据很长一段时间不会被访问,导致了整个LRU活跃链表被污染。等到下一次读到那些热点数据的时候,一下子发生了很多次磁盘I/O,性能急剧下降。
所以要想避免缓存污染,需要提高数据加入到活跃链表的门槛,保证在活跃链表中的数据不会被轻易替换掉。Linux操作系统中,当内存页被访问第二次才会将页从非活跃链表加入到活跃链表。
malloc(1024)是否会立刻占用物理内存?
不会,malloc仅修改进程的堆指针(通过brk和mmap),在虚拟地址空间中划出区域,物理内存尚未分配。当首次访问该内存时会触发缺页中断(page fault),然后内核才会分配物理页帧并建立页表映射。
Sharing is caring!