极客时间已完结课程限时免费阅读

09 | 程序装载:“640K内存”真的不够用么?

09 | 程序装载:“640K内存”真的不够用么?-极客时间

09 | 程序装载:“640K内存”真的不够用么?

讲述:徐文浩

时长10:21大小9.46M

计算机这个行业的历史上有过很多成功的预言,最著名的自然是“摩尔定律”。当然免不了的也有很多“失败”的预测,其中一个最著名的就是,比尔·盖茨在上世纪 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 系统设置的页的大小。
$ getconf PAGE_SIZE
由于内存空间都是预先划分好的,也就没有了不能使用的碎片,而只有被释放出来的很多 4KB 的页。即使内存空间不够,需要让现有的、正在运行的其他程序,通过内存交换释放出一些内存的页出来,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,让整个机器被内存交换的过程给卡住。
更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
实际上,我们的操作系统,的确是这么做的。当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于 CPU 的缺页错误(Page Fault)。我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存里。这种方式,使得我们可以运行那些远大于我们实际物理内存的程序。同时,这样一来,任何程序都不需要一次性加载完所有指令和数据,只需要加载当前需要用到就行了。
通过虚拟内存、内存交换和内存分页这三个技术的组合,我们最终得到了一个让程序不需要考虑实际的物理内存地址、大小和当前分配空间的解决方案。这些技术和方法,对于我们程序的编写、编译和链接过程都是透明的。这也是我们在计算机的软硬件开发中常用的一种方法,就是加入一个间接层
通过引入虚拟内存、页映射和内存交换,我们的程序本身,就不再需要考虑对应的真实的内存地址、程序加载、内存管理等问题了。任何一个程序,都只需要把内存当成是一块完整而连续的空间来直接使用。

总结延伸

现在回到开头我问你的问题,我们的电脑只要 640K 内存就够了吗?很显然,现在来看,比尔·盖茨的这个判断是不合理的,那为什么他会这么认为呢?因为他也是一个很优秀的程序员啊!
在虚拟内存、内存交换和内存分页这三者结合之下,你会发现,其实要运行一个程序,“必需”的内存是很少的。CPU 只需要执行当前的指令,极限情况下,内存也只需要加载一页就好了。再大的程序,也可以分成一页。每次,只在需要用到对应的数据和指令的时候,从硬盘上交换到内存里面来就好了。以我们现在 4K 内存一页的大小,640K 内存也能放下足足 160 页呢,也无怪乎在比尔·盖茨会说出“640K ought to be enough for anyone”这样的话。
不过呢,硬盘的访问速度比内存慢很多,所以我们现在的计算机,没有个几 G 的内存都不好意思和人打招呼。
那么,除了程序分页装载这种方式之外,我们还有其他优化内存使用的方式么?下一讲,我们就一起来看看“动态装载”,学习一下让两个不同的应用程序,共用一个共享程序库的办法。

推荐阅读

想要更深入地了解代码装载的详细过程,推荐你阅读《程序员的自我修养——链接、装载和库》的第 1 章和第 6 章。

课后思考

请你想一想,在 Java 这样使用虚拟机的编程语言里面,我们写的程序是怎么装载到内存里面来的呢?它也和我们讲的一样,是通过内存分页和内存交换的方式加载到内存里面来的么?
欢迎你在留言区写下你的思考和疑问,和大家一起探讨。你也可以把今天的文章分享给你朋友,和他一起学习和进步。
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 77

提建议

上一篇
08 | ELF和静态链接:为什么程序无法同时在Linux和Windows下运行?
下一篇
10 | 动态链接:程序内部的“共享单车”
unpreview
 写留言

精选留言(113)

  • 曾轼麟
    2019-05-15
    jvm已经是上层应用,无需考虑物理分页,一般更直接是考虑对象本身的空间大小,物理硬件管理统一由承载jvm的操纵系统去解决吧

    作者回复: 👍完全正确

    共 2 条评论
    137
  • DreamItPossible
    2019-08-11
    思考题 1. 在 Java 这样使用虚拟机的编程语言里面,我们写的程序是怎么装载到内存里面来的呢? 答:首先,我们编写的Java程序,即源代码`.java`文件经过编译生成字节码文件`.class`; 然后,创建JVM环境,即查找和装载`libjvm.so`文件; 最后,通过创建JVM实例,加载主类的字节码文件到系统给该JVM实例分配的内存中; 2. 它也和我们讲的一样,是通过内存分页和内存交换的方式加载到内存里面来的么? 答:Java代码的执行需要JVM环境,JVM环境的创建就是查找和装载`libjvm.so`文件:装载`libjvm.so`是通过内存分页和内存交换的方式加载到内存的。 字节码文件是通过类加载器加载到主类文件对应的JVM实例的内存空间中的,这一部分不是使用内存分页和内存交换的方式来管理的,使用的是JVM的内存分配策略来管理的;
    展开

    作者回复: 👍

    共 3 条评论
    94
  • 暴风雪
    2019-05-17
    老师,您好!通读全文,我有两个疑问想请假下您。 1.既然有了虚拟内存和物理内存作映射,为什么还要要求物理内存是连续的?如果不需要连续的物理内存,那么内存碎片的问题就不存在了。 2.

    作者回复: 暴风雪同学你好,映射不是一个byte一个byte来映射,而是映射一头一位的地址,不然映射表就太大了。映射到一头一尾中间的整段物理内存需要是连续的

    共 4 条评论
    50
  • 2019-05-16
    请教一下,按页分配就不需要连续内存空间了吗?进而,既然不需要连续,为什么还要再交换,不是随便放就好了吗?

    作者回复: 一页之内要连续,不同的页之间不需要。随便放内存里也放不下啊

    共 9 条评论
    45
  • humor
    2019-05-15
    既然操作系统本身有虚拟内存、内存交换和内存分页的能力,JVM为什么还要自己配置Heap等的大小呢?如果内存使用大于JVM配置的值,还会报OOM,反正有swap空间,让操作系统自己去做内存交换不就可以了吗?

    作者回复: JVM并不是一个系统级的程序啊,其实只是一个操作系统之上的应用程序,申请的这些heap size是确保自己只使用特定规模的资源啊

    40
  • 曾轼麟
    2019-05-15
    但是jvm中其实也会出现内存碎片的问题,所以也出现了各种各样的gc收集器

    作者回复: 其实分段之后换页合并,又何尝不是一种特殊情况下的垃圾回收呢

    共 2 条评论
    31
  • 子杨
    2019-05-21
    「我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存中」 这段话不太理解,虚拟内存不是指的程序中的内存地址吗?难道是实际存放在硬盘上的一段空间?那这和 Swap 分区有何关系吗?

    作者回复: 虚拟内存是指一段地址,但是没有加载到物理内存里的时候其实就是放在硬盘上。 你可以认为就是放在swap分区里面的,实际上是swap分区是一个历史遗留名词,现在“swap”的其实都是page了,当然也可以创建单独的.swp这样的文件。

    共 5 条评论
    21
  • Sherry
    2019-09-24
    老师,请问,一个程序是可以被打碎、装载到,n个不连续的页,去执行吗? 看到您回复别的同学说,一页之内内存空间要连续,不同页之间不需要。无论一页还是一段,都属于一块事先划定好的区域,内存一定连续。 但是一个程序假如使用了3个页,这3个页之间,地址也可以不连续吗? 如果是这样,页的交换又是用来解决什么问题的呢?
    展开

    作者回复: Sherry同学, 你好,当然可以不连续。页的交换是为了解决内存空间地址的管理问题啊。各个程序互相之间不能协调内存分配,所以要交给系统来分配。但是程序执行又需要知道内存地址,所以有了虚拟地址和物理地址的差别。同时内存空间可能不足以同时装载所有的程序,所以也需要页交换啊,把当前要运行的部分的数据加载到内存里面来。

    共 5 条评论
    20
  • Linuxer
    2019-05-15
    我想jvm也是一个可执行程序,应该同其他程序一样依赖于操作系统的内存管理和装载程序就可以了,它可以按自己的方式去规划它自身的内存空间给就java程序使用而无需考虑怎么映射到物理内存这些

    作者回复: 是的,没错

    20
  • 前端西瓜哥
    2019-07-20
    Mysql 的 B+树 实现,也考虑了页的问题呢,就是尽量将多个结点的数据保存在一起,让数据达到大概为1个页的大小(小于或等于)。这样每次就能从硬盘中读取尽可能接近一个页的数据到内存中,来减少 IO 操作。
    16
  • 不记年
    2019-05-16
    内存分页使得映射的基本单元从段变成了规范的,容易处理的页

    作者回复: 👍

    13
  • 剑衣清风
    2019-06-13
    觉得这章可以结合同是极客时间的 Linux性能优化实战 中的 15 | 基础篇:Linux内存是怎么工作的? 一起看
    共 1 条评论
    9
  • 美美
    2019-05-30
    分页是不连续的,那一个程序的多个页是怎么串联起来的?程序怎么做到顺序执行的?
    共 6 条评论
    8
  • 有铭
    2019-05-15
    为什么分页的默认大小是4KB,而不是2KB或者8KB?这里面应该是有某种理由的吧?

    作者回复: 分页的大小是可以在操作系统层面设置的,4k你可以认为是一般情况下的最佳实践,如果你的使用场景比较特殊,是可以设置成其他值的

    共 4 条评论
    8
  • 润曦
    2020-11-25
    内存交换只是为了把不活跃的内存占用交换到磁盘,来释放内存,以有效的利用内存。分页存在之前,内存交换是为了去掉内存碎片
    4
  • 活的潇洒
    2019-05-20
    从第1遍听到语言,到现在的笔记花了不少3个小时的时间,但是收获确实很多 刚完成笔记:https://www.cnblogs.com/luoahong/p/10894963.html

    作者回复: 👍加油

    共 3 条评论
    4
  • 西门吹牛
    2020-07-06
    程序在运行时,编写的代码其实是在很多文件中,首先将这多个目标文件,链接成一个可执行文件,这个可执行文件中包含指令和数据等信息,程序执行时,直接按照这个可执行文件中指令的顺序进行执行就行,但是cpu处理的是内存中的数据,如果把所有的指令和数据都加到内存,同时还要保证内存中的数据是连续存放的,这样很容易内存碎屏,内存也比较小,如果程序过大,直接装不下,更别谈运行了。 所以用内存分页的技术,将真实的内存空间和虚拟内存空间,都划分成很小的页,这里的虚拟内存,一般是用硬盘代替,程序会写存在硬盘中,指令对应的地址和数据对应的地址都是硬盘地址,运行的时候,把虚拟空间划分好的页,按顺序读取到内存中,通过映射关系找到对应的物流内存页,这样装载到内存的这一页数据,就可以在cpu的作用下执行,当需要下一页的时候,在装载下一页,避免了同时把整个应用程序都装载到内存; java中,程序是加载到虚拟机内存,装载到虚拟机内存的时候,是按对象加载,没有涉及内存分页和内存交换的问题,但是涉及了垃圾 回收,毕竟jvm内存是有限的,相当于这里的java 对象,类似内存分页中的每一页,jvm整个内存相当于计算机的内存 jvm可以当做是一个可运行的程序,java代码只是运行在这个程序上,而jvm运行时需要的内存,是靠承载jvm的操作系统去维护解决的,这个时候可能涉及内存交换和分页;
    展开
    3
  • Paul Shan
    2020-02-17
    Java虚拟机也管理内存,但是这是在操作系统之上的,Java虚拟机和操作系统分页系统的关注点不同在于,Java虚拟机关注哪些对象可以释放,以及选择合适的释放时机,避免释放内存的时候消耗太多CPU资源。操作系统更关注物理内存的使用,释放内存是程序的事情。Java虚拟机看重CPU和内存两者的综合性能,操作系统还要考虑硬盘。两者的单位也不同,Java虚拟机管理内存的单位是对象,而操作系统使用的单位是页。两者的相似之处是都要考虑CPU资源,都要最大程度的减少碎片,都要最大限度地减少内存的拷贝。
    展开
    3
  • Monday
    2020-02-16
    看了评论,表明本节的第一遍学习,只是让我初步的了解而已,没到掌握的层次。是要多读几遍

    作者回复: 👍

    3
  • 程序水果宝
    2020-02-12
    最近学计算机组成原理的原理的时候遇到了一些问题,老师说“在运行这些可执行文件的时候,我们其实是通过一个装载器,解析 ELF 或者 PE 格式的可执行文件。装载器会把对应的指令和数据加载到内存里面来,让 CPU 去执行”。 问题一:CPU怎么知道内存有需要执行的指令,操作系统是在加载程序到内存中时通过信号来通知CPU的吗? 问题二:CPU如何知道需要执行的指令在内存中的地址的,或者说操作系统如何将内存的地址放到CPU的PC寄存器的?
    展开

    作者回复: 程序水果宝同学, 你好 1. CPU不知道,CPU只是不停地根据PC寄存器不断往下执行程序。这个可以看一下后面的17讲开始的CPU部分 2. 整个计算机加电启动的过程,其实是我们的主板上的BIOS会从一个硬盘的固定位置开始加载引导程序开始的。 具体可以去读一读这篇文章 https://zhuanlan.zhihu.com/p/43802526

    3