内存
- 物理内存地址
- 存在硬件里面的空间地址
- 一个操作系统只有一份
- 虚拟内存地址
- 程序所使用的内存地址
- 每个程序一份
- 如果程序直接操作物理内存地址,一个操作系统只能运行一个程序
- 操作系统通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,如果找不到,会产生一个缺页异常
内存碎片
- 内部内存碎片:已分配给进程但没使用的内存(超量分配)
- 外部内存碎片:未被分配但是很小而无法分配的内存块
Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制。
直接使用段机制容易产生内存浪费和碎片(每次申请和使用都太大了) 只使用页管理,数据分布太散了,容易出现频繁IO
虚拟地址空间的内部被分为
- 内核空间
- 32位系统占用
1G,位于最高处 - 64位系统占用最高的
128T
- 32位系统占用
- 用户空间
- 32位系统占用剩下的
3G - 64位系统占用最低的
128T(64位剩余的中间部分是未定义的)- 注意这是虚拟地址,物理地址无限制,超过会出现问题,因为64位操作系统实际只使用了48位,其余未定义
- 32位系统占用剩下的
- 内核态可以访问两个空间,用户态只能访问用户空间
- 每个进程使用相同的内核空间,不同的用户空间(内核空间映射唯一)
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,才会晋升
- 预读失效:磁盘读取时会将多个 Page 缓存,减少 I/O 次数,但是不一定被访问,还会淘汰末尾可能的热点数据
LFU
Redis 使用,Redis 本身是内存数据库,不需要预读
- 每个记录有两个记录数据
- 记录的上次访问时间戳
- 记录的访问频次
- 每次记录被访问,会对访问频次进行衰减,如果上一次访问的时间与当前时间差距越大,衰减越大
- 衰减完对频次进行增加,频次越高的越难增加,类似于 log
