進程占用的內存可以有以下這些類型:
- 自身的代碼
- 共享庫的代碼
- 運行過程分配的堆和棧
- 通過mmap映射的磁盤文件內容
1. 虛擬內存與物理內存
這里要區分兩個概念,虛擬內存和物理內存。物理內存對于進程來說是透明的,進程直接操作的是虛擬內存。而數據和代碼是存放在真實的物理內存的,之所以進程在虛擬內存中尋址可以獲取數據,是因為虛擬內存與物理內存存在著映射關系。
當我們的進程向系統申請內存時,比如通過malloc方法,得到的其實是虛擬內存,如果進程沒有使用這些虛擬內存,那么它們是不會和物理內存關聯起來的。比如如果我們malloc 10MB的內存,但是只用了一個byte的,那么進程實際得到的只有一個頁的物理內存,也就是4096byte的內存空間。當物理內存被換出到磁盤(swap out),虛擬內存對應的地址還是有效的,如果尋址到這些地址,對應的物理內存就會被換入到內存(swap in)。
虛擬內存是連續的,而物理內存卻不一定。
2. 共享的內存
從進程自身的角度看,虛擬內存是進程獨立的,所有內存都是私有的,包括自身代碼、共享庫、堆棧等,它不用關心共享內存的事情。但實際上在物理內存的層面,很多東西是可以共享的,比如共享的代碼庫(.so)、自身代碼甚至是自身運行時私有的堆棧內存。
2.1 共享庫
同一個共享庫的代碼在物理內存中只會存在一份,這塊內存會映射到不同進程的虛擬內存中,對各個進程來說,就像是自己私有的內存一樣,而對于系統來說,則是節省了內存的資源。
2.2 進程自身的代碼
同樣,同一份代碼運行起來的多個進程是共享這些代碼的內存的,因為可執行代碼類型的內存頁是只讀的(除非是debugger模式),沒有必要復制多份,因此在實際場景中,當我們啟動一個大應用程序后,再啟動它的另一個實例,第二次啟動會快很多,這就是因為其代碼已經在內存中了,無需再重新加載一遍。
2.3 進程自身的私有內存
如果從一個進程fork出一個子進程,那么父進程的私有內存(比如堆棧)則會與子進程共享,但會被標記成(copy-on-write),意思是如果兩個進程都沒有修改這些內存頁,那么這些內存頁在兩個進程間就是共享的,但如果某個進程要修改某個頁了,那么這個頁就會先被復制一份,再被修改。
3. 進程內存的數據統計
在OSX系統,可以運行以下命令來查看某個進程內每一塊內存的類型(mapped file、Stack內存、malloc內存、代碼的__DATA或者__TEXT段等),以及大小、是否共享或者copy-on-write等。
sudo vmmap <pid>
如果只是關注RAM的內存,可以加上-resident參數:
sudo vmmap -resident <pid>
比如指定Firefox進程,有如下輸出:
可以看到,Firefox進程的代碼和庫代碼加載了108MB__TEXT段數據,字體支持(ATS)需要33MB內存,但只有2.5MB真正加載在物理內存中,它通過MALLOC申請了256MB內存,并且247MB在物理內存中,它有14MB用于棧內存,但只用了248KB。
4. 進程內存大小的度量方法
提到進程消耗的內存大小,我們或多或少聽到VSZ、RSS、PSS,那么它們代表的是什么呢?有了上述的知識背景,現在來分析或許能更加清晰。
4.1 VSZ(Virtual Memory Size)
指的是虛擬內存的大小,進程運行理論需要的內存大小,用這個來表示進程消耗了多少內存其實沒有太大的意義,因為它包含了未被加載到實際內存中的空間。舉個例子,假如有個文本編輯器叫做emacs,它有個編輯xml文件的功能,但這個功能比較少被用到,因為用戶一般情況下是編輯普通的文本,因此沒有必要一啟動就把這個功能的代碼加載到內存中。
除非用戶真實用到某個頁,否則系統不會把這個頁加載到內存,這其實稱為demand paging feature。
來看一下上面這個例子中虛擬內存工作的流程。首先啟動應用程序,系統為進程分配了運行編輯xml所需要的虛擬內存,但并沒有真正把這些功能所在的頁加載到物理內存。當進程真正調用到編輯xml的功能,CPU上的MMU模塊將會告訴系統,對應的虛擬內存頁發生缺頁了,那么系統就會暫停運行中的進程,把對應的頁加載到內存,再把這些物理內存頁映射到虛擬內存上,最后讓應用程序從暫停的地方繼續執行。對于進程來說,它是不知道自己被暫停了的,它只需要簡單地認為對應的功能已經加載在虛擬內存上了,并使用它就好了。
虛擬內存描述了進程運行時所需要的總內存大小,包括了那些還沒有被加載到實際內存中的代碼和數據。
4.2 RSS(Resident Set Size)
RSS表示了進程中真正被加載到物理內存中的頁的大小。但是用它來表示進程占用的內存大小也不太合適,因為還有個共享代碼庫的概念(Shared Libraries)。
比如libxml2.so這個程序庫,有多個進程會用到它,而系統在物理內存只會加載一遍這個代碼庫,然后這塊物理內存會被映射到不同進程的虛擬內存空間中,對于單獨的進程來說,就像是這個庫只加載在自己的虛擬內存中一樣,不需要關心它是否與其它進程共享。
而進程的RSS是包含這塊共享庫的內存空間的,因此如果簡單把系統中所有進程的RSS相加的話,結果是比系統總的內存大的,因為共享庫占的內存被計算了多遍。
4.3 PSS(Proportional Set Size)
PSS在VSS的基礎上,將共享庫的內存按使用的進程個數平均分成多份。假如有N個進程使用libxml2.so這個庫,這個庫加載了200K代碼在內存中,那么每個進程的PSS值中有(200 / N)K 的大小是這個共享庫貢獻的。
如果把系統中所有進程的PSS值加起來,就等于系統所有進程占用的內存總大小。
但是PSS并不是在所有的Linux系統中都有提供的,比如ps命令中就沒有PSS值,而Android的adb shell dumpsys meminfo <pid>
命令就可以看到進程的PSS值。
4.4 總結一下三種度量方法
如果要度量進程占用的內存大小,較好的選擇是使用PSS,用RSS也行,不過要注意有些內存是和別的進程共享的。
再舉個例子總結一下前面三個概念,比如一個進程有500K的代碼并且鏈接了2500K的共享庫,然后有200K的堆棧分配。其中有400K自身的代碼、1000K的共享庫以100K的堆棧內存被加載在實際內存(RAM)中,并且系統中一共有兩個進程用了同樣的共享庫。那么:
VSZ:500K + 2500K + 200K = 3200K
RSS:400K + 1000K + 100K = 1500K
PSS:400K + (1000K / 2) + 100K = 1000K
5. 命令執行結果
在Android的adb shell中執行ps命令的結果:
- 執行
adb shell ps
:
- 執行
adb shell dumpsys meminfo com.android.calendar
: