09 程序装载:“640K内存”真的不够用么?
计算机这个行业的历史上有过很多成功的预言,最著名的自然是“摩尔定律”。当然免不了的也有很多“失败”的预测,其中一个最著名的就是,比尔·盖茨在上世纪80年代说的“640K ought to be enough for anyone”,也就是“640K内存对哪个人来说都够用了”。
那个年代,微软开发的还是DOS操作系统,程序员们还在绞尽脑汁,想要用好这极为有限的640K内存。而现在,我手头的开发机已经是16G内存了,上升了一万倍还不止。那比尔·盖茨这句话在当时也是完全的无稽之谈么?有没有哪怕一点点的道理呢?这一讲里,我就和你一起来看一看。
程序装载面临的挑战
上一讲,我们看到了如何通过链接器,把多个文件合并成一个最终可执行文件。在运行这些可执行文件的时候,我们其实是通过一个装载器,解析ELF或者PE格式的可执行文件。装载器会把对应的指令和数据加载到内存里面来,让CPU去执行。
说起来只是装载到内存里面这一句话的事儿,实际上装载器需要满足两个要求。
第一,可执行程序加载后占用的内存空间应该是连续的。我们在第6讲讲过,执行指令的时候,程序计数器是顺序地一条一条指令执行下去。这也就意味着,这一条条指令需要连续地存储在一起。
第二,我们需要同时加载很多个程序,并且不能让程序自己规定在内存中加载的位置。虽然编译出来的指令里已经有了对应的各种各样的内存地址,但是实际加载的时候,我们其实没有办法确保,这个程序一定加载在哪一段内存地址上。因为我们现在的计算机通常会同时运行很多个程序,可能你想要的内存地址已经被其他加载了的程序占用了。
要满足这两个基本的要求,我们很容易想到一个办法。那就是我们可以在内存里面,找到一段连续的内存空间,然后分配给装载的程序,然后把这段连续的内存空间地址,和整个程序指令里指定的内存地址做一个映射。
我们把指令里用到的内存地址叫作虚拟内存地址(Virtual Memory Address),实际在内存硬件里面的空间地址,我们叫物理内存地址(Physical Memory Address)。
程序里有指令和各种内存地址,我们只需要关心虚拟内存地址就行了。对于任何一个程序来说,它看到的都是同样的内存地址。我们维护一个虚拟内存到物理内存的映射表,这样实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行。因为是连续的内存地址空间,所以我们只需要维护映射关系的起始地址和对应的空间大小就可以了。
内存分段
这种找出一段连续的物理内存和虚拟内存地址进行映射的方法,我们叫分段(Segmentation)。这里的段,就是指系统分配出来的那个连续的内存空间。
分段的办法很好,解决了程序本身不需要关心具体的物理内存地址的问题,但它也有一些不足之处,第一个就是内存碎片(Memory Fragmentation)的问题。
我们来看这样一个例子。我现在手头的这台电脑,有1GB的内存。我们先启动一个图形渲染程序,占用了512MB的内存,接着启动一个Chrome浏览器,占用了128MB内存,再启动一个Python程序,占用了256MB内存。这个时候,我们关掉Chrome,于是空闲内存还有1024 - 512 - 256 = 256MB。按理来说,我们有足够的空间再去装载一个200MB的程序。但是,这256MB的内存空间不是连续的,而是被分成了两段128MB的内存。因此,实际情况是,我们的程序没办法加载进来。
当然,这个我们也有办法解决。解决的办法叫内存交换(Memory Swapping)。
我们可以把Python程序占用的那256MB内存写到硬盘上,然后再从硬盘上读回来到内存里面。不过读回来的时候,我们不再把它加载到原来的位置,而是紧紧跟在那已经被占用了的512MB内存后面。这样,我们就有了连续的256MB内存空间,就可以去加载一个新的200MB的程序。如果你自己安装过Linux操作系统,你应该遇到过分配一个swap硬盘分区的问题。这块分出来的磁盘空间,其实就是专门给Linux操作系统进行内存交换用的。
虚拟内存、分段,再加上内存交换,看起来似乎已经解决了计算机同时装载运行很多个程序的问题。不过,你千万不要大意,这三者的组合仍然会遇到一个性能瓶颈。硬盘的访问速度要比内存慢很多,而每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。所以,如果内存交换的时候,交换的是一个很占内存空间的程序,这样整个机器都会显得卡顿。
内存分页
既然问题出在内存碎片和内存交换的空间太大上,那么解决问题的办法就是,少出现一些内存碎片。另外,当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,这样就可以解决这个问题。这个办法,在现在计算机的内存管理里面,就叫作内存分页(Paging)。
和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小。而对应的程序所需要占用的虚拟内存空间,也会同样切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。从虚拟内存到物理内存的映射,不再是拿整段连续的内存的物理地址,而是按照一个一个页来的。页的尺寸一般远远小于整个程序的大小。在Linux下,我们通常只设置成4KB。你可以通过命令看看你手头的Linux系统设置的页的大小。
由于内存空间都是预先划分好的,也就没有了不能使用的碎片,而只有被释放出来的很多4KB的页。即使内存空间不够,需要让现有的、正在运行的其他程序,通过内存交换释放出一些内存的页出来,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,让整个机器被内存交换的过程给卡住。
更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
实际上,我们的操作系统,的确是这么做的。当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于CPU的缺页错误(Page Fault)。我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存里。这种方式,使得我们可以运行那些远大于我们实际物理内存的程序。同时,这样一来,任何程序都不需要一次性加载完所有指令和数据,只需要加载当前需要用到就行了。
通过虚拟内存、内存交换和内存分页这三个技术的组合,我们最终得到了一个让程序不需要考虑实际的物理内存地址、大小和当前分配空间的解决方案。这些技术和方法,对于我们程序的编写、编译和链接过程都是透明的。这也是我们在计算机的软硬件开发中常用的一种方法,就是加入一个间接层。
通过引入虚拟内存、页映射和内存交换,我们的程序本身,就不再需要考虑对应的真实的内存地址、程序加载、内存管理等问题了。任何一个程序,都只需要把内存当成是一块完整而连续的空间来直接使用。
总结延伸
现在回到开头我问你的问题,我们的电脑只要640K内存就够了吗?很显然,现在来看,比尔·盖茨的这个判断是不合理的,那为什么他会这么认为呢?因为他也是一个很优秀的程序员啊!
在虚拟内存、内存交换和内存分页这三者结合之下,你会发现,其实要运行一个程序,“必需”的内存是很少的。CPU只需要执行当前的指令,极限情况下,内存也只需要加载一页就好了。再大的程序,也可以分成一页。每次,只在需要用到对应的数据和指令的时候,从硬盘上交换到内存里面来就好了。以我们现在4K内存一页的大小,640K内存也能放下足足160页呢,也无怪乎在比尔·盖茨会说出“640K ought to be enough for anyone”这样的话。
不过呢,硬盘的访问速度比内存慢很多,所以我们现在的计算机,没有个几G的内存都不好意思和人打招呼。
那么,除了程序分页装载这种方式之外,我们还有其他优化内存使用的方式么?下一讲,我们就一起来看看“动态装载”,学习一下让两个不同的应用程序,共用一个共享程序库的办法。
推荐阅读
想要更深入地了解代码装载的详细过程,推荐你阅读《程序员的自我修养——链接、装载和库》的第1章和第6章。
课后思考
请你想一想,在Java这样使用虚拟机的编程语言里面,我们写的程序是怎么装载到内存里面来的呢?它也和我们讲的一样,是通过内存分页和内存交换的方式加载到内存里面来的么?
欢迎你在留言区写下你的思考和疑问,和大家一起探讨。你也可以把今天的文章分享给你朋友,和他一起学习和进步。
- 曾轼麟 👍(150) 💬(3)
jvm已经是上层应用,无需考虑物理分页,一般更直接是考虑对象本身的空间大小,物理硬件管理统一由承载jvm的操纵系统去解决吧
2019-05-15 - haoly1989 👍(102) 💬(3)
思考题 1. 在 Java 这样使用虚拟机的编程语言里面,我们写的程序是怎么装载到内存里面来的呢? 答:首先,我们编写的Java程序,即源代码`.java`文件经过编译生成字节码文件`.class`; 然后,创建JVM环境,即查找和装载`libjvm.so`文件; 最后,通过创建JVM实例,加载主类的字节码文件到系统给该JVM实例分配的内存中; 2. 它也和我们讲的一样,是通过内存分页和内存交换的方式加载到内存里面来的么? 答:Java代码的执行需要JVM环境,JVM环境的创建就是查找和装载`libjvm.so`文件:装载`libjvm.so`是通过内存分页和内存交换的方式加载到内存的。 字节码文件是通过类加载器加载到主类文件对应的JVM实例的内存空间中的,这一部分不是使用内存分页和内存交换的方式来管理的,使用的是JVM的内存分配策略来管理的;
2019-08-11 - Geek_vqdpe4 👍(58) 💬(4)
老师,您好!通读全文,我有两个疑问想请假下您。 1.既然有了虚拟内存和物理内存作映射,为什么还要要求物理内存是连续的?如果不需要连续的物理内存,那么内存碎片的问题就不存在了。 2.
2019-05-17 - 徐 👍(46) 💬(10)
请教一下,按页分配就不需要连续内存空间了吗?进而,既然不需要连续,为什么还要再交换,不是随便放就好了吗?
2019-05-16 - humor 👍(40) 💬(1)
既然操作系统本身有虚拟内存、内存交换和内存分页的能力,JVM为什么还要自己配置Heap等的大小呢?如果内存使用大于JVM配置的值,还会报OOM,反正有swap空间,让操作系统自己去做内存交换不就可以了吗?
2019-05-15 - 曾轼麟 👍(33) 💬(2)
但是jvm中其实也会出现内存碎片的问题,所以也出现了各种各样的gc收集器
2019-05-15 - Sherry 👍(25) 💬(5)
老师,请问,一个程序是可以被打碎、装载到,n个不连续的页,去执行吗? 看到您回复别的同学说,一页之内内存空间要连续,不同页之间不需要。无论一页还是一段,都属于一块事先划定好的区域,内存一定连续。 但是一个程序假如使用了3个页,这3个页之间,地址也可以不连续吗? 如果是这样,页的交换又是用来解决什么问题的呢?
2019-09-24 - 子杨 👍(23) 💬(6)
「我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存中」 这段话不太理解,虚拟内存不是指的程序中的内存地址吗?难道是实际存放在硬盘上的一段空间?那这和 Swap 分区有何关系吗?
2019-05-21 - Linuxer 👍(22) 💬(1)
我想jvm也是一个可执行程序,应该同其他程序一样依赖于操作系统的内存管理和装载程序就可以了,它可以按自己的方式去规划它自身的内存空间给就java程序使用而无需考虑怎么映射到物理内存这些
2019-05-15 - 不记年 👍(13) 💬(1)
内存分页使得映射的基本单元从段变成了规范的,容易处理的页
2019-05-16 - 有铭 👍(9) 💬(5)
为什么分页的默认大小是4KB,而不是2KB或者8KB?这里面应该是有某种理由的吧?
2019-05-15 - 程序水果宝 👍(5) 💬(1)
最近学计算机组成原理的原理的时候遇到了一些问题,老师说“在运行这些可执行文件的时候,我们其实是通过一个装载器,解析 ELF 或者 PE 格式的可执行文件。装载器会把对应的指令和数据加载到内存里面来,让 CPU 去执行”。 问题一:CPU怎么知道内存有需要执行的指令,操作系统是在加载程序到内存中时通过信号来通知CPU的吗? 问题二:CPU如何知道需要执行的指令在内存中的地址的,或者说操作系统如何将内存的地址放到CPU的PC寄存器的?
2020-02-12 - 活的潇洒 👍(5) 💬(3)
从第1遍听到语言,到现在的笔记花了不少3个小时的时间,但是收获确实很多 刚完成笔记:https://www.cnblogs.com/luoahong/p/10894963.html
2019-05-20 - Monday 👍(4) 💬(1)
看了评论,表明本节的第一遍学习,只是让我初步的了解而已,没到掌握的层次。是要多读几遍
2020-02-16 - yang 👍(3) 💬(1)
老师讲的太好了 我没学过计算机组成原理 看了部分评论和回复,我想问一个问题: 老师,java申请的所谓的一块连续的堆内存a,在连续的堆内存a上开辟一段连续的空间b, 这个a 和 b 都是在 物理内存m上的嘛? 我的意思是: 堆内存a对外表现是连续的,但是其实在物理内存m上,是由多个内存页page组成的吧? 堆内存a上开辟的连续的空间b,也是在物理内存m上,多个固定大小且连续的内存页page组成的吧? 那这样的话,不管是堆内存a,还是被开辟出来的空间b, 是不是都有被交换到swap上的可能啊? (我也不知道我问的对不对,但是我就是根据您的问题想问一下,或许后面有答疑,或许没有...) 想请老师回答一下,谢谢您~!
2020-01-02