特注:本文为参加2018年12月SHLUG聚会的笔记。

https://www.kernel.org/doc/gorman/html/understand/understand005.html

Linux的内存模型总体上一句话总结叫做“分层,抽象”

从上(顶层更面向应用)到下(底层更物理)分为

  • memblock

  • mem\_section

  • node

  • zone

  • page

  • free list

  • slub

简单来说是为了以下需求来分的:

  • 对物理进行抽象隔离,简化上层访问

  • 管理方便,支持热插拔

  • 预先索引好空闲的空间,便于加快分配速度

  • 按不同应用情景分好,便于满足不同的申请要求

这个模型原则,实际上与网络协议、硬件总线的设计思路是一致的。

一句话调侃:没有什么问题是分层不能解决的,如果有,那就再分一层。

一些额外概念

  • struct page (这个可以去看内核源代码) 对于内存页的meta信息,用于追踪内存页的状态等

  • NUMA 一种计算机硬件的架构,特点是一组CPU与一组内存总线“直连”。在跨总线访问内存时存在性能差异。

  • 对内存的申请有一个全局总线锁,在这条总线上的CPU都会受到影响,即瓶颈

题外话

关于内核的OOM Killer

问题:为什么程序申请“虚拟”内存的空间有2^64(64位系统中),而实际上还是会“因为内存不足被KILL”?

实际上内存分配成功失败,与 OOM Killer 是分开两个不同的事件,在事件顺序上没有固定关系。

  1. 申请内存失败的主要原因是,由于用户态程序向内核申请内存时,可能会触发内核内部其他的内存分配。导致实际上可能超过真正的可分配内存。

所以内核会有一个阈值,当接近这个阈值时,系统调用返回 NO MORE ,表示不可分配。

  1. 已分配的内存状态中分为(还有其他)可被回收( reclaimable )与不可回收( unreclaimable ),在申请内存时,实际上也会触发内核对可回收内存的回收,综合结果进行返回。

而实际上一些特殊的程序的内存一直会被标记为USED,即内存正在使用。当一个操作系统中物理内存都在使用时,那就无法继续分配。

  1. 当内存使用量接近那个阈值时,内核会启动一个OOM Killer进程,对当前用户态进程进行打分,根据某种算法计算出需要杀掉的进程,杀掉,然后在syslog中打日志。故两个动作其实是分开的。

  2. 提到SWAP做热交换,实际上这个问题比较玄学。并不是开了swap就一定会使用。也不是配置了不使用swap就不使用。具体需要去看swap的实现。

对于目前这个年代的应用,swap最大的应用可能只有hibernate(休眠到硬盘,断电)。

  1. 一部分涉密的内存内容是不可以写到硬盘(即SWAP出去)的,例如tty的login-manager,这部分内存就只能驻留内存。实际上可以申请内存为保留(reserved)。但是所有内存都常驻不回收的话,内存不足就只能系统崩溃了。

golang 的内存分配

源代码:runtime/malloc.go

壳叔的说法是256KiB,这里查到的资料,tiny<16字节,small是<32KiB,超过的都算大对象。golang vm会先留好几个池子。其中mspan是分配单位。

  • mspan 存放多个slot,针对tiny对象预先留好slot位置,分不同大小需求使用

  • mcache 是对mspan的快速索引

  • mcentral 保留一个mspan列表,其中有空闲空间提供使用

  • mheap 堆内存,最大的预分配池子

mspan有3种状态

  • idle 没有对象占用,可提供golang内部使用,或释放回OS,或者用于栈内存

  • in use 至少被一个堆对象占用,可能还有空间存放其他对象

  • stack 给goroutine使用的栈空间,可以被栈或者堆使用(两者其一)

  1. 分配tiny时

    • 如果能找到合适的,与现有的挤在一起,放进mcache

    • 如果没有合适的,就从mcache获取新的mspan,检查是否有可用的tiny slot,并放进去,这步操作不需要加锁

    • 如果没有可用的tiny slot,则从mcentral的mspan list中取走一个新的mspan使用

    • 如果mspan list已经空,则从mheap再申请新的mspan

    • 如果mheap也用空,则向OS申请新的内存页

  2. 分配small时,跳过检查tiny slot的步骤,其他一样

  3. 分配其他大对象,会直接通过mheap,即堆内存操作。

这样设计的目的是,减少对OS频繁地内存申请,以及降低并发申请的瓶颈(小对象通过“可用内存slot索引”快速分配,不加锁)

JVM的内存分配

JVM开启时根据配置先分配好的内存进行JVM内部的分配,JVM内存模型是分代垃圾回收。当JVM的内存池不够时,再继续向OS申请。JVM内部的分配和操作系统的内存分配实际上没有必然关联。

\_\_END\_\_