早期系统
多个进程全部装入内存、内存撑爆。并且多个程序都在用户态,相互存在打扰,不小心访问到别人的内存
解决内存撑爆问题
【截图】
分页
首先我们把程序分成一块一块的,分块的大小是固定的,这个分块叫做内存页(程序最基本的标准内存页,大小为 4K,虽然现在的内存页大小已经有16K,64K等,但最基本的大小是 4K。有的系统是不支持大分页的,但是4K的分页都支持)。
与此同时,在我的内存里头,也把内存空间分成一块一块的框框,叫做页框。用到哪个分页,装哪个。
也就是说现在内存关于进程的管理,实际上已经把进程在逻辑上给大卸八块了。所以我们平时的程序,我要load到内存里去运行了,它真的已经被load到内存了吗?你说QQ.exe,我要放到内存里去运行啦,它真正的会被装到内存吗?不是。实际上我只是把QQ.exe 进行分页搞出一个页表,同时告诉进程调度器说,有一个进程需要进来啊,它的可执行入口是在第三页上(假设有6页)。然后进程调度器说OK,我这里正好有一块空间,注意只有一块!麻烦你把第三页拿进来就行了,那么第三块就可以执行了,在执行过程中,第三块需要第四块的数据,旁边还有一块空间,第四块内容加载进来了。
交换分区
问题是,如果我内存已经全满了,第四块要用第五块了,这时候怎么办?把那个最不常用的块 swap 交换出去,放到交换分区swap。Linux有个特殊的分区叫做交换分区,干什么使的,就是把内存当中最不常用的那些个块扔到里边去。当然这是(交换分区)用硬盘实现的,它的效率比较低。但是用这样的一种方式,就能够支持更多的更令话的进程管理。这就是交换分区的由来。
当一个进程刚刚load到内存,还没有开始执行的时候,操作系统给它分配的资源,甚至连内存都没有,只是给它分配了一张表,说我知道了你分了几页,每页都干了什么。现在的操作系统就是这么干的,用到哪页加载哪页。当内存已满的时候,新加载的块,会把最不常用的块交换到swap交换内存。如果swap交换内存上的数据又要被用到,也是同理,再把最老的交换出去。这个算法就是著名的LRU算法。
头条面试第三轮 手撕 LRU 算法
leetcode 146题 视频161节1小时05分
LRU (Least Recently Used) 最近最不常用算法。找到最不常用的那一块,把它替换出去,就是这么一个意思。
解题思路:
- 哈希表(保证查找操作O(1)),链表(保证把链表中的某个节点换到尾部的操作为O(1))
- 双向链表(保证把链表中的某节点换到尾部前,从原来表中删除的操作O(1))
解决相互打扰问题:虚拟地址空间
虚拟内存
【截图】
- 为了保证互不影响,让进程工作在虚拟空间。虚拟空间,并不是真正的空间,你在里面读写到的地址都是虚的,不是真正的物理地址。
- 程序中用到的地址空间不再是直接的物理地址,而是虚拟的地址,这样A进程永远不可能访问到B进程的空间。
- 虚拟地址多大呢?就是你的寻址空间大小,如果是64位系统,大小就是2^64 bit,比物理空间大很多。
- 站在虚拟的角度,进程是独享整个系统(2^64 位)+CPU。
- 线性地址通过 OS + MMU(硬件 Memory Management Unit 内存管理单元),来映射到真正的物理地址上的。
为什么使用虚拟内存
【截图】
内存映射
再说一遍,站在进程角度,它自己占有一块巨大的、独一无二的不和别人共享的空间。但是这个空间是虚拟的,怎么把虚拟的内存空间和真实的物理空间,一一对应起来,叫做内存的映射。
怎么进行内存映射的呢?
这里有几个概念:
- 逻辑地址:相当于偏移量
- 线性地址:相当于基地址+偏移量
- 物理地址:真正的硬件上的地址
线性地址映射到物理地址,靠的是 MMU。也就是说作为一个进程,你永远无法知道自己真正的物理地址是哪一块,只有操作系统自己知道。这就变得非常的安全了。
【截图】
上图, P1、P2、P3、P4,4个进程,站在他们的角度,它们都是独占整个内核的,实际上不是这么一回事儿,内核幻化出了很多幻影,让大家伙儿认为每个人都是独享她,这是一个水性扬花的女子,同时应付着很多人。然后她和另外一个哥们为相配合,这个哥们儿就是 MMU。MMU呢,为每一个进程分配内存空间,如果内存空间不够用了,就利用LRU算法,把不需要用的内存页儿,扔到硬盘上的交换分区。
一个进程要寻找地址的时候,要经过操作系统,要经过MMU的转换,才能得到真正的地址。这样操作系统就会变得非常非常的安全。
总结
【Java程序员需要掌握的操作系统底层.md】 ZGC之上的内容。