在DOS时代 ,内存很小,同一时间只能有一个进程在运行(也有一些特殊算法可以支持多进程,通过栈来手动实现多进程之间的切换,但这个比较特殊很少见,不做讨论)
内存变大,可以让多个进程装入内存。但依然存在问题:
1、内存不够用,多个进程同时装入,但内存时有限的。
2、互相打扰,因为多个进程同时存在于内存中,那意味着某一个进程可以随意去访问另外一个进程的物理内存地址,这也是非常危险的一件事情,恶意攻击将变得非常容易。
为了解决这两个问题,诞生了现在的内存管理系统:虚拟地址 分页装入 软硬件结合寻址
针对于内存不够的问题,采用了内存分页的解决方法。
为了解决之前内存不够用的问题以及涉及到内存对齐的问题(每个进程的所需内存大小不同,可能会因为某个进程的内存比较大,即使内存有剩余,但也放不下改进程的所需大小),现代内存管理系统采用的解决方法是: 首先在内存中,不再是一整块作为单位进行存储管理,而是分为若干个不同的固定大小的内存小块进行存储管理,这其实也就是所谓的内存页,内存页以4K作为一个标准的页大小进行划分。现今的操作系统都实现了以4K为最小单位页的存储粒度,当然有些操作系统还支持了4K的倍数大小(16k,64k)的单位页的存储。 因此最初即使没有任何进程的时候,内存中其实也已经划分成了一个个页框,等待着进程的载入
。
而与此同时,程序(硬盘)也分为一块块4K大小的块。 并且程序读入内存时,不会一次性将块全部放入内存中,而是实现了一种按需放入:程序运行时需要哪些块,此时才放入内存中
。
也就是说,一个进程在启动的时候,操作系统只是分配并记录了这个进程对应的所有的磁盘页块映射表,并且发现例如启动程序所依赖的数据代码处于3号磁盘块,此时就把进程的3号磁盘块load到内存中,然后发现load进来的3号磁盘块的代码还依赖了4号磁盘块,就同时把4号也load到内存中。。。。 这样的一个过程 ,而并非全部load进来
。
但即使是这样,如果load块的时候,内存还是已经满了,已经放不下一个新的内存页了怎么办?操作系统会将内存中最长时间没有使用的内存块从内存取出放入swap分区中(当然swap分区是硬盘实现的,速度肯定非常慢),然后再将刚刚这个新的要存入的程序块进行load到对应刚刚被移走的那个内存位置上。 而这个操作的策略,实际上就是LRU算法。
关于LRU算法的具体实现思路,可以参考我的另外一篇笔记:
leetcode146-LRU算法.note
而针对于进程之间相互打扰的问题,采用了虚拟内存的解决方法。
为了保证进程之间互不影响,也为了保证物理内存的安全性,操作系统虚拟出来了一块内存空间,每个进程都工作在自身独有的虚拟内存空间中,自此,每个进程都不再能直接去访问直接内存,从而A进程永远不可能再可以访问到B进程的内存空间,也就解决了内存互相访问的安全性的问题
。
那虚拟内存空间多大呢?寻址空间 , 64位系统来说,是 2 ^ 64 byte,比物理内存空间大很多 。
站在虚拟的角度,每个进程都认为自己是独享整个系统 + CPU
虚拟内存的结构是固定的,而它的结构主要是由若干段组成的:
1、数据段,又分为只读的数据段和可读写的代码段
2、代码段,只读
3、运行时堆
4、共享库,用来存放例如一些调用c的类库
5、用户栈
6、内核。物理真实的内核只有一个,但站在每个进程的虚拟角度来看,自身都拥有一个完整的内核。
基于每一段中,又分为了若干的内存页,这个内存页的概念和真实内存的概念相同,因此也是按需load程序块进虚拟内存页。
每个进程都可以在自己的虚拟内存中看到这样的一个结构,对进程来说操作的都是虚拟内存中的内容,其实最终对虚拟空间的操作还是要作用在真实内存地址中,
因此就需要将对应的虚拟地址与真实的内存地址进行一个映射
因此这里我们可以做一个小结:
在虚拟内存中,一个数据肯定是被装在了某个内存数据段中的某个内存页中,逻辑地址就是说这个数据相对于所在数据段的偏移位置
找到数据的所在内存页偏移地址,先需要找到数据所在的内存段的地址,因为内存段也是一整块虚拟内存中的某个偏移位置上,因此所谓线性地址说的就是:
数据所在内存段的偏移位置(基地址)+数据相对于所在数据段的偏移位置(也就是逻辑地址,也叫偏移量)。
假设数据的逻辑地址是20,所在数据段地址是1000,那么它的线性地址就是1000+20=1020
当然,上面说的地址都是虚拟空间上的地址,那如何映射到真实内存地址上呢?实际上这个过程是比较复杂的。
整个过程其实是需要数据从逻辑地址->线性地址->物理地址的一个映射过程。这涉及到需要把虚拟空间的页放入到真实内存中去,如果真实内存不够用,还需要把真实内存置换出去等等的问题。
但总结来讲,这个过程的完成,是操作系统+硬件(CPU中的MMU:Memory Management Unit)来完成的。
每个进程中的虚拟地址都需要操作系统+MMU来完成映射到物理空间内存中,如果物理内存装不下,会放置到硬盘级别的swap分区中。
因为一个进程的内存不会全量分配,因此会存在例如load磁盘3号块到了内存中,结果发现3号要用到磁盘4号块的内容,但此时内存页中还没有,就会产生缺页异常(中断),由内核处理并加载4号块到内存页中。
有时候我们发现电脑运行的时候,忽然变卡,然后响了起来。 这个时候可能就是因为内存满了,对内存中的数据进行置换出去,然后将需要的内容从磁盘读入内存中,所以会比较慢,但却是必需的。
ZGC使用的垃圾回收算法叫做:Colored Pointer
GC信息记录在对象指针上,而不是记录在对象头部, immediate memory use(内存立即重用),区别于之前的记录在对象头部,每当对象的状态进行了修改之后,都需要去更新markword的状态,从而才能判断对象是否存活,是否可以从内存中回收。而ZGC将GC信息直接记录在对象指针上,通过发现对象指针的改变,可以不再需要去看markword,直接就可以决定是否在内存中进行释放这个对象的空间。
ZGC现在只支持64位的系统,因此对象指针是64位的,Colored Pointer使用了42位作为对象指针的寻址空间,也就是4T,除此之外,还有4位存放对象的此时的状态,18位是空余的。因此我们不难想到,后续可以慢慢扩增对象指针的存储位数,因此, 在JDK13,更是提高到了44位进行存储,也就是16T。 但实际上,也最高只能使用44位进行存储寻址地址,这是为什么呢?
这其实牵扯到一个问题:
一条数据数字和一条指令,本质上都是01进行组合的数字,那CPU如何区分是指令还是数据?
实际上总线内部分为:数据总线 地址总线 控制总线,通过不同的总线穿过来区分不同类型。
而我们的寻址自然是用到的是地址总线,而地址总线目前设计上只设计了48位。
颜色指针本质上包含了地址映射的概念,ZGC运用了虚拟内存,通过在不同的内存区域存放不同状态的对象,最终映射到同一块物理地址上面去。