内存分配过程
应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
当应用程序读写了这块虚拟内存,CPU就会去访问这个虚拟内存,这时会发现这个虚拟内存没有映射到物理内存,CPU就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler (缺页中断函数)处理。
缺页中断处理函数会看是否有空间的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
如果没有空闲的物理内存,那么内核就会开始回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。
- 后台内存回收:在物理内存紧张的时候,会唤醒kswapd内核线程来回收内存,这个回收内存的过程是异步的,不会阻塞进程的执行。
- 直接内存回收:如果后台异步回收赶不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会触发OOM(out of memory)机制
OOM Killer机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
申请物理内存的过程如下图:
哪些内存可以被回收?
系统内存紧张的时候,就会进行回收内存的工作,具体那些内存会被回收呢?
主要有两类内存可以被回收,而且它们回收的方式也不同。
- 文件页:内核缓存的磁盘数据和内核缓存的文件数据都叫做文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。
- 匿名页:这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过Linux的Swap机制,Swap会把不常访问的内存先写到磁盘中,然后释放掉这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
文件页和匿名页的回收都是基于LRU算法,也就是优先回收不经常访问的内存。LRU回收算法实际上维护着active和inactive两个双向链表,其中:
- active_list 活跃内存页链表,这里存放的是最近被访问过的(活跃)的内存页
- inactive_list 不活跃内存链表,这里存放的是很少被访问的(非活跃)的内存页
越靠近链表尾部,就表示内存页越不常访问。这样在回收内存时,系统就可以根据活跃度,优先回收不活跃的内存。
回收内存带来的性能影响
回收内存有两种方式:
- 一种是后台内存回收,也就是唤醒kswapd内核线程,这种方式是异步回收的,不会阻塞进程
- 一种是直接内存回收,这种方式是同步回收的,会阻塞进程,这样就会造成很长时间的延迟,以及系统的CPU利用率会升高,最终引起系统负荷飙高。
可被回收的内存类型由文件页和匿名页:
- 文件页的回收:对于干净页是直接释放内存,这个操作不会影响性能,而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘I/O的,这个操作会影响系统性能。
- 匿名页的回收:如果开启了Swap机制,那么Swap机制会将不常访问的匿名页换出到磁盘中,下次访问时,再从磁盘换入到内存中,这个操作是会影响系统性能的。
可以看出,回收内存的操作基本都会发生磁盘I/O的,如果回收内存的操作很频繁,意味着磁盘I/O次数会很多,这个过程势必会影响系统的性能,整个系统给人的感觉就是很卡。
如何保护一个进程不被OOM杀掉?
在系统空闲内存不足的情况下,进程申请了一个很大的内存,如果直接回收内存都无法回收出足够大的空间内存,那么就会触发OOM机制,内核就会根据算法选择一个进程杀掉。
进程得分的结果受下面两个方面影响:
- 第一,进程已经使用的物理内存页面数
- 第二,每个进程的OOM校准值oom_score_adj。它是可以通过 /proc/[pid]/oom_score_adj 来配置的。我们可以设置 -1000 ~ 1000之间的任意一个数值,调整进程被 OOM Kill的几率。
函数 oom_badness() 里的最终计算方法是:
// points 代表打分的结果
// process_pages 代表进程已经使用的物理内存页面数
// oom_score_adj 代表 OOM 校准值
// totalpages 代表系统总的可用页面数
points = process_pages + oom_score_adj*totalpages/1000
用[系统总的可用页面数] 乘以 [OOM 校准值 oom_score_adj] 再除以 1000,最后再加上进程已经使用的物理页面数,计算出来的值越大,那么这个进程被 OOM Kill的几率也就越大。
每个进程的oom_score_adj的默认值都是 0 ,所以最终得分跟进程自身消耗的内存有关,消耗的内存越大越容易被杀掉。我们可以通过调整 oom_score_adj 的数值,来修改进程的得分结果:
- 如果不想某个进程被首先杀掉,那就可以调整该进程的oom_score_adj,从而改变这个进程的得分结果,降低该进程被OOM杀死的概率。
- 如果想让某个程序无论如何都不能被杀掉,就可以将oom_score_adj配置为-1000.
我们最好将一些很重要的系统服务的oom_score_adj配置为-1000,比如sshd,因为这些系统服务一旦被杀掉,我们就很难再登录进系统了。
但是不建议将我们自己的业务程序的oom_score_adj配置为-1000,因为业务程序一旦发生了内存泄漏,而它又不能被杀掉,这就会导致随着它的内存开销变大,OOM killer不停地被唤醒,从而把其他进程一个个给杀掉。
总结
内核在给应用程序分配物理内存的时候,如果空闲物理内存不够,那么就会进行内存回收的工作,主要有两种方式:
- 后台内存回收:物理内存紧张的时候,会唤醒kswapd内核线程来回收内存,这个回收内存的过程是异步的,不会阻塞进程的执行
- 直接内存回收:如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
可被回收的内存类型由文件页和匿名页:
- 文件页的回收:对于干净页式直接释放内存,这个操作不会影响性能,而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘I/O的,这个操作会影响系统性能
- 匿名页的回收:如果开启了Swap机制,那么Swap机制会将不常访问的匿名页换出到磁盘中,下次访问时,再从磁盘换入内存中,这个操作会影响性能
文件页和匿名页的回收都是基于LRU算法,也就是优先回收不常访问的内存。回收内存的操作基本都会发生磁盘I/O的,如果回收内存的操作很频繁,意味着磁盘I/O次数会很多,这个过程势必会影响系统的性能。
针对回收内存导致的性能影响,常见的解决方式:
- 设置 /proc/sys/vm/swappiness,调整文件页和匿名页的回收倾向,尽量倾向于回收文件页;
- 设置 /proc/sys/vm/min_free_kbytes,调整 kswapd 内核线程异步回收内存的时机;
- 设置 /proc/sys/vm/zone_reclaim_mode,调整 NUMA 架构下内存回收策略,建议设置为 0,这样在回收本地内存之前,会在其他 Node 寻找空闲内存,从而避免在系统还有很多空闲内存的情况下,因本地 Node 的本地内存不足,发生频繁直接内存回收导致性能下降的问题;
如果在经历完直接内存回收后,空闲的物理内存大小依然不够,那么就会触发OOM机制,OOM killer会根据每个进程的内存占用情况和oom_score_adj 的进行打分,得分最高的进程就会被首先杀掉。
可以通过调整oom_score_adj的值,来降低被 OOM killer杀掉的几率。