比爾·蓋茨在上世紀80年代說的「640K ought to be enough for anyone」程序員
也就是「640K內存對哪一個人來講都夠用了」編程
那個年代,微軟開發的仍是DOS操做系統,程序員們還在絞盡腦汁,想要用好這極爲有限的640K內存segmentfault
而如今,我手頭的Mac Book Pro已是16G內存了,上升了一萬倍還不止。瀏覽器
那比爾·蓋茨這句話在當時也是徹底的無稽之談麼?有沒有哪怕一點點的道理呢?這一講裏,我就和你一塊兒來看一看。app
在運行這些可執行文件的時候,咱們實際上是經過一個裝載器,解析ELF或者PE格式的可執行文件jvm
裝載器會把對應的指令和數據加載到內存裏面來,讓CPU去執行。編程語言
裝載到內存,裝載器須要知足兩個要求性能
執行指令的時候,程序計數器是順序地一條一條指令執行。這意味着,這一條條指令須要連續地存儲在一塊兒學習
雖然編譯出來的指令裏已經有了對應的各類各樣的內存地址,可是實際加載的時候,咱們其實沒有辦法確保,這個程序必定加載在哪一段內存地址上
由於如今的計算機一般會同時運行不少個程序,可能你想要的內存地址已經被其餘加載了的程序佔用優化
要知足這兩個基本的要求,咱們很容易想到一個辦法。那就是咱們能夠在內存裏面,找到一段連續的內存空間,而後分配給裝載的程序,而後把這段連續的內存空間地址,和整個程序指令裏指定的內存地址作一個映射。
指令裏用到的內存地址叫做虛擬內存地址(Virtual Memory Address)
實際在內存硬件裏面的空間地址,咱們叫物理內存地址(Physical Memory Address)
程序裏有指令和各類內存地址,咱們只須要關心虛擬內存地址就好了
對於任何一個程序來講,它看到的都是一樣的內存地址。咱們維護一個虛擬內存到物理內存的映射表,這樣實際程序指令執行的時候,會經過虛擬內存地址,找到對應的物理內存地址,而後執行。由於是連續的內存地址空間,因此咱們只須要維護映射關係的起始地址和對應的空間大小就能夠了。
這種找出一段連續的物理內存和虛擬內存地址進行映射的方法,咱們叫分段(Segmentation)。
這裏的段,就是指系統分配出來的那個連續的內存空間。
分段的辦法很好,解決了程序自己不須要關心具體的物理內存地址的問題,但它也有一些不足之處,第一個就是內存碎片(Memory Fragmentation)
舉個例子
電腦有1GB的內存
先啓動一個圖形渲染程序,佔用了512MB的內存
接着啓動一個Chrome瀏覽器,佔用了128MB內存
再啓動一個PY程序,佔用了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這樣使用虛擬機的編程語言裏面,咱們寫的程序是怎麼裝載到內存裏面來的呢?它也和咱們講的同樣,是經過內存分頁和內存交換的方式加載到內存裏面來的麼?
jvm已是上層應用,無需考慮物理分頁,通常更直接是考慮對象自己的空間大小,物理硬件管理統一由承載jvm的操縱系統去解決吧
深刻淺出計算機組成原理