程序装载

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

程序装载面临的挑战

通过链接器,把多个文件合并为一个最终可执行文件。在执行这些可执行文件的时候,就是通过一个装载器,解析ELF 或 PE 格式的可执行文件。装载器把对应的指令和数据加到内存中,让CPU 去执行。

  • 所以装载器实际上要满足两个要求

一、可执行程序加载后占用的内存空间应该是连续的,在执行指令的时候,程序计数器是顺序的一条一条指令执行下去,这也意味着,这些指令需要连续的存储在一起。

二、我们需要同时加载多个程序,并且不能让程序自己规定在内存中加载的位置。虽然编译出来的指令有了对应的各种各样的内存地址,但是实际加载的时候,是没有办法确保,这个程序一定加载在那一段内存地址上。因为我们现在的计算机通常会同时运行很多个程序,可能你想要的内存地址已经被其他加载了的程序占用了。

要满足这两个基本的要求,一个办法就是我们可以在内存里面,找到一段连续的内存空间,然后分配给装载的程序,然后把这段连续的内存空间地址,和整个程序指令里指定的内存地址做一个映射。

把指令里用到的内存地址叫作虚拟内存地址(Virtual Memory Address),实际在内存硬件里面的空间地址,我们叫物理内存地址(Physical Memory Address)。

程序里有指令和各种内存地址,我们只需要关心虚拟内存地址就行了。对于任何一个程序来说,看到的都是同样的内存地址。我们维护一个虚拟内存到物理内存的映射表,这样实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行。

因为是连续的内存地址空间,所以我们只需要维护映射关系的起始地址和对应的空间大小。

内存分段

这种找出一段连续的物理内存和虚拟内存地址进行映射的方法,我们叫分段(Segmentation) 。这里的段,指的是系统分配出来的那个连续的内存空间。

内存分段

分段存在一个问题 就是内存碎片。

我们来看这样一个例子。我现在手头的这台电脑,有 1GB 的内存。我们先启动一个图形渲染程序,占用了 512MB 的内存,接着启动一个 Chrome 浏览器,占用了 128MB 内存,再启动一个 Python 程序,占用了 256MB 内存。这个时候,我们关掉 Chrome,于是空闲内存还有 1024 - 512 - 256 = 256MB。按理来说,我们有足够的空间再去装载一个 200MB 的程序。但是,这 256MB 的内存空间不是连续的,而是被分成了两段 128MB 的内存。因此,实际情况是,我们的程序没办法加载进来。

内部碎片

这个也有办法解决。解决的办法叫内存交换(Memory Swapping)。

(Memory swapping )可以将Python 程序占用的那256MB 的内存写到硬盘上,然后再从硬盘读回到内存里。读回来的时候,它被加载到那个占用了的512MB内存的后面。这样,我们就有了连续的 256MB 内存空间,就可以去加载一个新的 200MB 的程序。

  • 虚拟内存、分段再加上内存交换,看似解决了计算机同时装载多个程序的问题,但是会遇到性能瓶颈。硬盘访问速度比内存要慢的多,每一次内存交换都需要把一大段的连续内存数据写到磁盘上,所以交换非常占内存空间,会引起卡顿。

内存分页

为了少出现一些内存碎片,另外,当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,在现在计算机的内存管理里面,就叫作内存分页(Paging)。

和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小(4kB)。而对应程序所需要占用的虚拟内存空间,也同样切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。从虚拟内存到物理内存的映射,不再是拿整段连续的内存的物理地址,而是按照一个一个页来的。页的尺寸一般远远小于整个程序的大小

由于内存空间都是预先划分好的,也就没有了不能使用的碎片,而只有被释放出来的很多 4KB 的页。即使内存空间不够,需要让现有的、正在运行的其他程序,通过内存交换释放出一些内存的页出来,一次写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,让整个机器被内存交换过程给卡住。

内存分页

更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。而是只在程序运行中,需要用到对应虚拟内存页里的指令和数据是,再加载到物理内存里面去。

当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于 CPU 的缺页错误(Page Fault)。当 OS 捕捉到这个错误时,将对应的页从存放在硬盘上的虚拟内存读取出来,加载到物理内存里。这种方式,使得我们可以运行那些远大于我们实际物理内存的程序(只需要加载到当前需要用到的就行)

  • 通过虚拟内存、内存交换和内存分页这三个技术的组合,我们得到了一个让程序不需要考虑实际的物理内存地址、大小和当前分配空间的解决方案。(这些技术和方法,对于我们程序的编写、编译和链接过程都是透明的。这也是我们在计算机的软硬件开发中常用的一种方法,就是加入一个间接层。)
  • 任何一个程序,都只需要把内存当成是一块完整而连续的空间来直接使用。

总结延伸

实要运行一个程序,“必需”的内存是很少的。CPU 只需要执行当前的指令,极限情况下,内存也只需要加载一页就好了。再大的程序,也可以分成一页。每次,只在需要用到对应的数据和指令的时候,从硬盘上交换到内存里面来就好了。以我们现在 4K 内存一页的大小,640K 内存也能放下足足 160 页呢,也无怪乎在比尔·盖茨会说出“640K ought to be enough for anyone”这样的话。

不过呢,硬盘的访问速度比内存慢很多,所以我们现在的计算机,没有个几 G 的内存都不好意思和人打招呼。

除了程序分页装载这种方式之外,我们还有其他优化内存使用的方式么?其实“动态装载”也是可以优化内存使用。

思考:

在 Java 这样使用虚拟机的编程语言里面,我们写的程序是怎么装载到内存里面来的呢?它也和我们讲的一样,是通过内存分页和内存交换的方式加载到内存里面来的么?

一、首先,我们编写的Java程序,即源代码.java文件经过编译生成字节码文件.class

然后,创建JVM环境,即查找和装载libjvm.so文件;

最后,通过创建JVM实例,加载主类的字节码文件到系统给该JVM实例分配的内存中;

二、ava代码的执行需要JVM环境,JVM环境的创建就是查找和装载libjvm.so文件:装载libjvm.so是通过内存分页和内存交换的方式加载到内存的。
字节码文件是通过类加载器加载到主类文件对应的JVM实例的内存空间中的,这一部分不是使用内存分页和内存交换的方式来管理的,使用的是JVM的内存分配策略来管理的;

-------------本文结束感谢您的阅读-------------
作者水平有限,文中难免存在一些错误,欢迎邮件@交流讨论~
Zongpeng Lin 微信 微信
Zongpeng Lin 支付宝 支付宝