Skip to content

内存

  • 物理内存地址
    • 存在硬件里面的空间地址
    • 一个操作系统只有一份
  • 虚拟内存地址
    • 程序所使用的内存地址
    • 每个程序一份
    • 如果程序直接操作物理内存地址,一个操作系统只能运行一个程序
    • 操作系统通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,如果找不到,会产生一个缺页异常

内存碎片

  • 内部内存碎片:已分配给进程但没使用的内存(超量分配)
  • 外部内存碎片:未被分配但是很小而无法分配的内存块

Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制

直接使用段机制容易产生内存浪费和碎片(每次申请和使用都太大了) 只使用页管理,数据分布太散了,容易出现频繁IO

虚拟地址空间的内部被分为

  • 内核空间
    • 32位系统占用1G,位于最高处
    • 64位系统占用最高的 128T
  • 用户空间
    • 32位系统占用剩下的 3G
    • 64位系统占用最低的 128T(64位剩余的中间部分是未定义的)
      • 注意这是虚拟地址,物理地址无限制,超过会出现问题,因为64位操作系统实际只使用了48位,其余未定义
  • 内核态可以访问两个空间,用户态只能访问用户空间
  • 每个进程使用相同的内核空间,不同的用户空间(内核空间映射唯一)

Linux 内存分配和回收

  • 应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
  • 当虚拟内存被读写,如果发现没有映射到物理内存,CPU 就会产生缺页中断,进程会从用户态切换到内核态。缺页中断进行物理内存分配
    • 如果有,就直接分配物理内存,并建立映射关系。
    • 如果没有,进行回收内存的工作
      • 后台内存回收:
        • 异步回收,不影响进程执行
      • 直接内存回收
        • 后台内存回收来不及,进行同步回收
      • 如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么会触发 OOM(Out of Memory)机制
        • 根据算法选择一个占用物理内存较高的进程杀死
        • 会一直杀死直到内存足够了
        • 可以调整进程的 oom_score_adj 参数让其避免被杀死,但是普通用户进行不建议,防止出现内存泄漏且不会被杀死
      • 可以被回收的内存
        • 文件页:磁盘数据
          • 未修改的称为干净页,直接释放即可
          • 修改过的称为脏页,落盘后释放
        • 匿名页:无对应磁盘数据,不能直接释放
          • 通过swap机制把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。 如果在4GB 物理内存的机器上,申请 8G 内存
  • 32 位操作系统,进程最多只能申请 3 GB 大小的虚拟内存空间,所以进程申请 8GB 内存的话,在申请虚拟内存阶段就会失败
  • 64 位操作系统
    • 如果没有开启 Swap 机制,程序就会直接 OOM
    • 如果有开启 Swap 机制,程序可以正常运行
    • 最大的申请量不确定,和磁盘也有关

缓存

都是内存空间的数据

  • Linux 操作系统的缓存:加速访问数据
  • MySQL 缓存:提升数据库读写能力,读和 Linux 缓存类似,写的脏页由后台线程写入磁盘

LRU

传统的 LRU 算法:(没有直接被 Linux 和 MySQL 使用)

  • 当访问的页在内存里,就直接把该页对应的 LRU 链表节点移动到链表的头部。
  • 当访问的页不在内存里,除了要把该页放入到 LRU 链表的头部,还要淘汰 LRU 链表末尾的页。
    • 如果内存够,就不用淘汰末尾的页
  • 会有以下问题导致缓存命中率下降:
    • 预读失效:磁盘读取时会将多个 Page 缓存,减少 I/O 次数,但是不一定被访问,还会淘汰末尾可能的热点数据
      • 解决方案就是分代,一个活跃区,一个非活跃区
      • 未被访问过的预读页只会进入非活跃区,访问后才会进入活跃区,避免影响活跃区的热点数据
      • Linux 和 Mysql 都是这么实现的
    • 缓存污染:当批量读取数据时,数据可能都只被访问一次就不再访问了(如全表遍历),导致活跃区的数据全部被淘汰
      • 增加非活跃区数据加入活跃区的门槛
      • Linux 是当内存页第二次被访问才会晋升
      • MySQL 是当内存页第二次被访问且与第一次访问间隔大于 1s,才会晋升

LFU

Redis 使用,Redis 本身是内存数据库,不需要预读

  • 每个记录有两个记录数据
    • 记录的上次访问时间戳
    • 记录的访问频次
  • 每次记录被访问,会对访问频次进行衰减,如果上一次访问的时间与当前时间差距越大,衰减越大
  • 衰减完对频次进行增加,频次越高的越难增加,类似于 log

正在精进