应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
当应用程序读写了这块虚拟内存,CPU就会去访问这个虚拟内存,这时会发现这个虚拟内存没有映射到物理内存,CPU就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler (缺页中断函数)处理。
缺页中断处理函数会看是否有空间的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
如果没有空闲的物理内存,那么内核就会开始回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。
如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会触发OOM(out of memory)机制
OOM Killer机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
申请物理内存的过程如下图:
系统内存紧张的时候,就会进行回收内存的工作,具体那些内存会被回收呢?
主要有两类内存可以被回收,而且它们回收的方式也不同。
文件页和匿名页的回收都是基于LRU算法,也就是优先回收不经常访问的内存。LRU回收算法实际上维护着active和inactive两个双向链表,其中:
越靠近链表尾部,就表示内存页越不常访问。这样在回收内存时,系统就可以根据活跃度,优先回收不活跃的内存。
回收内存有两种方式:
可被回收的内存类型由文件页和匿名页:
可以看出,回收内存的操作基本都会发生磁盘I/O的,如果回收内存的操作很频繁,意味着磁盘I/O次数会很多,这个过程势必会影响系统的性能,整个系统给人的感觉就是很卡。
在系统空闲内存不足的情况下,进程申请了一个很大的内存,如果直接回收内存都无法回收出足够大的空间内存,那么就会触发OOM机制,内核就会根据算法选择一个进程杀掉。
进程得分的结果受下面两个方面影响:
函数 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配置为-1000,比如sshd,因为这些系统服务一旦被杀掉,我们就很难再登录进系统了。
但是不建议将我们自己的业务程序的oom_score_adj配置为-1000,因为业务程序一旦发生了内存泄漏,而它又不能被杀掉,这就会导致随着它的内存开销变大,OOM killer不停地被唤醒,从而把其他进程一个个给杀掉。
内核在给应用程序分配物理内存的时候,如果空闲物理内存不够,那么就会进行内存回收的工作,主要有两种方式:
可被回收的内存类型由文件页和匿名页:
文件页和匿名页的回收都是基于LRU算法,也就是优先回收不常访问的内存。回收内存的操作基本都会发生磁盘I/O的,如果回收内存的操作很频繁,意味着磁盘I/O次数会很多,这个过程势必会影响系统的性能。
针对回收内存导致的性能影响,常见的解决方式:
如果在经历完直接内存回收后,空闲的物理内存大小依然不够,那么就会触发OOM机制,OOM killer会根据每个进程的内存占用情况和oom_score_adj 的进行打分,得分最高的进程就会被首先杀掉。
可以通过调整oom_score_adj的值,来降低被 OOM killer杀掉的几率。