虚拟内存

27 阅读17分钟

Virtual Memory

关于top命令

top - 17:10:58 up 5:24, 0 users, load avegrage: 0.09, 0.10, 0.09 
Tasks: 28 total, 1 running, 27 sleeping, 0 stopped, 0 zombie
%Cpu(s): 1.0 us, 1.3 sy, 0.0 ni, 97.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.2 st
KiB Men : - total, - free, - used, - buff/cache
KiB Swap: - total, - free, - used, - avail Men
​
PID  USER   PR NI   VIRT    RES     SHR S %SPU %MEM     TIME+ COMMAND
6489 admin  20 0  991.7m   4.0g   72864 S 3.7  50.1  1859.67 java

首先让我们来解释一下top命令的输出内容

  • VIRT

    VIRT is the virtual memory space: the sum of everything in the virtual memory map (see below). It is largely meaningless, except when it isn't (see below).

    VIRT指虚拟内存空间,是虚拟内存映射中所有内容的总大小作用不大,看个乐就行

  • RES

    RES is the resident set size: the number of pages that are currently resident in RAM. In almost all cases, this is the only number that you should use when saying "too big." But it's still not a very good number, especially when talking about Java.

    所有页在RAM中的大小,但是对于java进程来说可能不是那么准确

  • SHR

    SHR is the amount of resident memory that is shared with other processes. For a Java process, this is typically limited to shared libraries and memory-mapped JARfiles. In this example, I only had one Java process running, so I suspect that the 7k is a result of libraries used by the OS.

    共享内存空间的大小,对于java进程来说关联共享libraries和内存映射JARfiles

理解Virtual Memory Map

通过pmap指令查询当前进程空间的映射,对于java进程来说,其中包含了数据(例如java heap) shared libraries和memory-mapped JARfiles。

0000000040000000     36K r-x--  /usr/local/java/jdk-1.6-x64/bin/java
0000000040108000      8K rwx--  /usr/local/java/jdk-1.6-x64/bin/java
0000000040eba000    676K rwx--    [ anon ]
00000006fae00000  21248K rwx--    [ anon ]
00000006fc2c0000  62720K rwx--    [ anon ]
0000000700000000 699072K rwx--    [ anon ]
000000072aab0000 2097152K rwx--    [ anon ]
00000007aaab0000 349504K rwx--    [ anon ]
00000007c0000000 1048576K rwx--    [ anon ]
...
00007fa1ed00d000   1652K r-xs-  /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar
...
00007fa1ed1d3000   1024K rwx--    [ anon ]
00007fa1ed2d3000      4K -----    [ anon ]
00007fa1ed2d4000   1024K rwx--    [ anon ]
00007fa1ed3d4000      4K -----    [ anon ]
...
00007fa1f20d3000    164K r-x--  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f20fc000   1020K -----  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f21fb000     28K rwx--  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
...
00007fa1f34aa000   1576K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3634000   2044K -----  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3833000     16K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3837000      4K rwx--  /lib/x86_64-linux-gnu/libc-2.13.so
​
地址                  大小   权限                 anon或文件

第一部分是JVM加载器,这部分会加载到shared libraries。

然后是一组anon,这部分就是java heap。JVM在分配虚拟内存空间基于参数-Xmx分配一个连续空间;-Xms参数则设定在JVM在程序启动时可用的heap大小

memory-mapped JARfile,将jar映射到内存中,从而更加高效的读取文件,在上述示例中,映射的jar就是rt.jar。JVM将会把classpath上所有的jar都映射到内存中

线程私有数据。示例中给出了2个线程,1M的栈和4k不知道干嘛用的内存块(可能是用来限制栈大小的,因为这部分没有读写权限,当栈超过1M后想要访问后续内存就会因为权限问题而报错)

虚拟内存

软件在操作系统上运行需要一个很简单的前提:内存。操作系统以RAM的形式为运行的软件提供内存。内存的大小也随着不同的软件而变化,有的软件需要很多内存,而有的软件只需要很少一部分。大多数用户会在操作系统上运行多个应用,因为操作系统的内存总量有限,因为分配的内存就格外珍贵。为了满组所有同时运行的应用的内存需求,操作系统有两点格外关注:

  1. 应用应当始终运行,除非用户主动关闭
  2. 在上述应用运行过程中,还要可观测其性能

现在主要的问题回归到如何管理内存,如何精确管理那一部分内存属于哪一个应用

可能的解决方案1:

让每一个应用都显式的声明运行时需要的内存大小,例如PS声明在运行时需要从0到1023的地址空间,也就是1GB的内存

这样做的好处就是所有的程序都会预先分配内存,当程序安装或运行时,只需要将数据放在预先分配好的内存中即可。当然这样做也有缺点,因为程序所需要的内存不是一成不变的,所以分配固定的内存不能应对应用内存波动;此外,操作系统上的应用可能有很多,我们的内存可能无法为所有应用都分配其想要的大小、

可能的解决方案2:

让操作系统专注于内存管理。当应用需要内存的时候,先向操作系统请求,再由操作系统统一分配。操作系统为了保证无论何时应用启动都有足量的内存,在分配内存的时候就会优先从低位地址分配(简单来说,RAM可以理解为一个线性的数组,对于4GB的内存来说,地址空间就是0-232-1的子节数组)。当有应用启动,或者运行中的应用扩容,操作系统优先从上一次分配的地址接续。因为应用访问地址的时候并不关心实际的物理地址在哪,所以操作系统为每一个应用维护一张逻辑地址到物理地址的映射表

现在假设你刚刚启动系统,还没有启动任何应用,然后你打开了VLC,系统为VLC从低位分配了一部分内存;然后你开着视频又想打开浏览器上网冲浪;接着你又打开notePad记事本;最后你又打开了eclipse打算敲代码。现在你的RAM满了,还可能是这样子:

缺个图 线性空间满了

问题1:

因为RAM满了,所以无法在启动任何应用。所以应用必须时刻牢记最大可用内存,也就是说,1GB内存的电脑无法运行高内存消耗的应用,如PS 原神等

那么现在你决定不在运行eclipse和chrome,为了节约空间你关闭这两个进程,他们运行时占据的内存会被操作系统回收,现在内存看起来会像

不连续的空余内存

假设关了这两个进程为你节省了700M(400+300)的空间,现在你想要开启Opera需要450M的空间。从总量上说是完全可以打开的,但是问题在于不是连续的内存空间,都是一些离散且大小不超过450的内存块。这时我们就需要整理一下内存空间,从低位开始排列,最后剩下700M的空余内存在高位,这一过程称之为compaction。通过compaction我们解决了内存不连续的问题,除了被我们移动的内存仍有运行的进程在使用。操作系统维护一张逻辑地址和物理地址的映射表。假设进程在逻辑地址45存储了数据123,操作系统实际把123存放在了物理地址2012,那么操作系统会维持一个45->2012的映射关系。如果我们对内存整理,曾经在物理地址2012的数据换到了新的位置,所以当我们整理内存的时候也需要同步修改映射表。对于所有拥有地址的元素,所有函数和对象都会遇到这种问题,移动进程的内存就意味着这些地址全都发生了变化,这样必然引入新的问题。

问题2:

我们不能移动进程,改变这些地址的代价很大。所以在进程退出后会在内存中留下许多空洞,这些内存空洞被称为External Fragmentation(外部碎片)

现在假设我们采取了某些神奇的手法完美的移动了这些进程的地址空间,现在又有700M的空闲空间

高位留700M

Opera非常完美的占用了我们刚整理出来的内存,现在RAM

满的

此时再想重新打开chrome就会发现内存已经一滴也没有了。但是进程所占用的内存并非一成不变的,例如VLC播放的视频暂停。所占用的内存小于播放状态下需要的内存。这样看来,原笨一滴不剩的RAM还是有空余的

满的但是还有碎片

不同于External Fragmentation因为进程终止而产生,这一次碎片的产生是因为应用内存的波动。不过无论因为何种原因,我们又一次遇到这个相同的问题:离散内存的总和大于需要分配的内存,但是因为地址不连续导致无法分配足够的内存。所以我们不得不花费高昂的代价去整理这些碎片,更加糟糕的是,在整个程序的生命周期内,内存的波动是非常频繁的。

问题4:

应哟程序在整个生命周期中,可能降低其内存的使用,留下未被使用但是可以进一步利用的内存碎片,需要我们去整理这些未被使用的空间。这些碎片称为Internal Fragmentation

现在我们的OS来执行内存整理,移动进程并且启动Chrome,现在我们的RAM看起来

又满了

假设我们还想用PS从外部硬盘加载几张图片。要知道,硬盘的访问要比缓存和RAM访问慢得多,这是一项I/O操作,也就是说并不需要用到RAM内存,但是现状是应用仍然占用RAM空间。我们是否能在I/O操作期间进一步释放空余内存,为其他应用提供内存。

问题5:

I/O操作期间仍占用内存,利用率降低,这部分内存可以分配给同时发生在I/O期间的CPU任务

我们现在来看一下操作系统是如何解决这两个问题的。进程的虚拟地址空间会映射到物理内存块,也就是page(页)。页大小4kb。逻辑地址和物理地址的映射关系通过page table维护。给出一个虚拟地址以后,首先找到这个地址属于那一页,然后再根据映射表找到页所对应的物理地址(frame),因为地址在page和frame中的偏移量是一致的,于是根据地址偏移量+映射表计算出虚拟地址对应的物理地址

左侧是进程的虚拟内存空间,现在虚拟内存需要40单位空间。右侧是物理内存空间,如果物理内存空间正好有40个单位空间,那么这是最好的,只需要把所有空余都分配给这个进程就可以了。但是不出意外的话肯定就出意外了,现在物理内存只有24个单位,所以进程间不得不共享一部分内存。

假设现在进程启动,想要访问地址35的内存数据,page大小8(每个page包含8个单位,整个进程需要40个单位内存,也就是5页)。不难算出35属于第4页(35/8)。在这一页中访问地址的偏移量是3(35%8),对于当前要访问的地址(35)来说,我们可以用二元组来表示(pageIndex, offset)。因为进程刚启动,还没有数据存储到物理内存中,所以映射表还是空的。现在OS让出CPU,通过驱动访问磁盘为进程读取第4页(也就是在磁盘上的一部分存储块,对应虚拟内存地址32-39).随后OS在RAM中为这一页分配空间,将page加载到内存中,也是RAM中的第一个frame,然后映射表维护虚拟内存和物理内存的关系,因为page 4对应frame 0,当进程再此访问(4, 3),映射表给出page 4已经对应物理内存frame 0,在frame上偏移3个单位就是需要访问的数据。

现在再假设进程想要访问地址28,也就是(3, 4)。显然映射表没有关于page 3的关系·,需要再此让出CPU从磁盘中加载数据,然后更新映射关系。现在映射表中又朵了page 3对应frame 1的关系。然后进程再访问地址8(1, 0),OS再次加载数据,映射表新增page 1对应frame 2。现在RAM仅有的24单位空间都被沾满了。当进程想要访问30(3, 6),因为映射表中已经有关于page 3的记录,不需要再访问磁盘读取数据。

随后进程想要访问地址(0, 3),这样就会出现问题,因为映射表中不存在page 0的记录而且RAM空间还满了,不能再加载磁盘中的数据。不过这并不能妨碍虚拟内存的正常运作,因为虚拟内存又内存驱逐的设计(决定驱逐哪块内存有不同的策略可选,例如LRU,最近被频繁访问的内存会被留下,不经常访问的frame就会被删除;还有first-come-first-evicted,最先加载的最先驱逐)。假设我们当前遵循随机原则,将frame 1从内存中驱逐(frame 1对于进程来说仍然映射着page 3)。因此OS不得不通知进程,frame 1已经不在属于你,它已经被RAM中驱逐为其他page让出空间;而进程必须保证这一消息更新到映射表中,将于被驱逐frame相关的映射删除。当下一次进程再次访问page 3,映射表就会告诉进程需要去磁盘读取page。现在随着frame 1从内存中驱逐,page 0换入,映射表也修改新的映射关系为page 0对应frame 1,映射关系看起来会是这样的

映射

那么现在进程想要增长内存空间,不再像之前需要移动所有进程来整理空间,仅仅涉及单个page在内存中交换。这使得进程不在依赖连续内存空间,即使是离散的空间也可以。OS维持其中的映射关系,当进程需要的时候OS会告诉进程去哪里读取数据。不过也许你会想,如果一个数据每次读取都查找不到,那岂不是每次都要从磁盘中读取?理论上说是的,不过大多数编译器都遵循locality of reference设计的。如果使用某个内存中的数据,那么下一个数据的地址和这个数据的地址非常接近,甚至都在同一个page中。所以查询不到的情况并没有那么频繁。大多数数据读取的需求都会在刚刚换入的page或者最近换入的page中得到满足;同样,如果page最近没有被使用过,那么接下来也大概不会再使用这个page及其附近的数据。当然这个结论并不绝对,总会有特数场景影响到性能。

问题4的解决方案:

如果进程面临内存问题,只需要简单做page1交换,不需要移动其他进程,从而让内存增长变得简单

问题1的解决方案:

进程可以访问无限的内存。当所需要内存大于空余内存,磁盘将作为备份,需要的数据从磁盘加载到内存,不经常使用的数据从内存转移到磁盘。这个过程可以无限的持续下去,因为磁盘的空间更加廉价还更大,从而给进程造成了空间无限的假象。这也是虚拟内存名称的由来,OS给出虚拟的内存,但实际上并不存在这么多内存空间。

之前我们提到过,虽然进程主动减少所需的内存,但是这些被释放的内存仍让难以被其他进程所使用。但是现在不同了,当进程占用的内存减小,那么所对应的page也就不会在被访问,根据LRU原则这些page就会从内存中驱逐,换入其他进程所需要的page,并且这些操作并不会带来高昂的代价

问题3的解决方案:

如果进程减少占用内存,那么对应的frame在RAM中访问频率也随之减低,所以LRU将驱逐这些frame,并换入其他进程所需要的page,这样就避免了内存碎片以及代价高昂的compaction操作

对于外部碎片的问题也有很好的解决。现在已经不需要整理内存空间来容纳新的进程,只要找到page存放读取的位置。在page维度下,不存在hole这个概念,也不需要对RAM进行整理。

问题2的解决方案:

为了容纳新的进程,只需要将其他进程不经常访问的数据,以page为单位从内存中驱逐。所以现在没有了hole或者外部碎片的概念

那么现在进程进行I/O操作也可以更轻易的让出CPU使用。OS将这部分page驱逐,换入其他需要的page;当I/O操作结束,OS只需要再将这些page从磁盘中加载进来。

问题5的解决方案:

当进程进行I/O操作时,可以更加简单的让出内存占用,便于其他进程充分利用I/O操作期间的空余内存

现在所有进程都不会直接访问RAM,所有进程都访问虚拟内存,虚拟地址映射到物理地址,映射关系由进程自行维护。OS搞速进程那些frame是空闲的,可以存放进程需要的page。因为内存分配被OS监管,所以可以通过只分配空闲frame来保证进程间不会互相影响,或者再占用其他进程内存时通过及时维护进程映射表来进行通信。

原始问题的解决方案:

因为内存由OS管理,所以进程不会侵占其他进程占用的内存空间,每个进程运行再自己虚拟sandbox中


原文链接 传送门链接

原文链接 传送门链接.