虛擬內存的來由
一個系統中的進程是與其他進程共享CPU和主存資源的,最開始我們直接訪問物理內存地址,但是后來我們發現會造成各種各樣的問題:
- 地址空間不隔離
所有的進程都可以直接訪問物理地址,那表明各個進程的內存空間不是互相隔離的。有些惡意的進程或者被注入惡意代碼的進程非常容易去改寫其他進程的內存數據,以達到破壞的目的。- 內存使用效率低
由于沒有有效的內存管理機制,需要一個程序執行時,會將整個程序裝入內存中然后開始執行。如果我們這個時候突然想要運行另外一個程序,那么很可能遇到內存空間不足。這時候有一種處理方法是將其他程序的數據暫時寫到磁盤里面,等到用的時候再讀回來。由于程序所需要的空間是連續的,那么在這個方法里,如果我們將程序A換出到磁盤所釋放的內存空間是不夠的,所以接著會將程序B換出到磁盤,然后將程序C讀入到內存開始運行。我們可以看出來,整個過程中有大量的數據在換入換出,導致效率十分低下。- 程序運行的地址不確定
因為程序每次需要裝入運行時,我們需要給它從內存中分配一塊足夠大的空閑區域,這個空閑區域的位置是不確定的。這給程序員的編寫造成了一定的麻煩,因為程序在編寫時,它訪問數據和指令跳轉的目標地址很多都是固定的,需要重定向。
這時候,就產生了一種解決方案,一種對主存的抽象概念,叫做 虛擬內存(Virtual Memory/VM,下文中為了簡便可能會使用縮寫) 。
虛擬內存的作用
虛擬內存是硬件異常、硬件地址翻譯、主存、磁盤文件和內核軟件的完美交互,它為每個進程提供了一個大的、一致的和私有的地址空間。
虛擬內存提供可三個重要的功能:
- 它將主存看成是一個存儲在磁盤上的地址空間的高速緩存,在主存中只保存活動區域,并根據需要在磁盤和主存之間來回傳送數據;
- 它為每個進程提供了一致的地址空間,從而簡化了內存管理;
- 它保護了每個進程的地址空間不被其他進程破壞。
VM是沉默的工作,不需要開發人員的任何干涉。但是,我們依然要注意它,原因有三:
- 虛擬內存是核心的
VM遍及計算機系統的所有層面,在硬件異常、匯編器、鏈接器、加載器、共享對象、文件和進程的設計中扮演著重要的角色。理解VM將幫助開發者更好的理解系統通常是如何工作的。(尤其是在iOS開發中!)- 虛擬內存是強大的
VM給予了應用程序強大的能力,可以創建和銷毀內存片、將內存片映射到磁盤文件中的某個部分(mmap),以及與其他進程共享內存。理解VM將幫助你利用它的強大功能在應用程序中添加動力。- 虛擬內存是危險的
每次應用程序引用一個變量、間接引用一個指針,或者調用一個諸如malloc這樣的動態分配程序時,它就會和VM發生交互。如果VM使用不當,應用將遇到復雜危險的與內存有關的錯誤。理解VM可以幫助開發者規避這種錯誤。
尋址方式
計算機系統的主存被組織成一個由M個連續的字節大小的單元組成的數組。每個字節都有一個唯一的物理地址(Physical Address)。第一個字節的地址為0,下一個為1,在往下是2,以此類推。直接通過物理地址訪問內存的方法就是 物理尋址。原理 如下圖:
而現在除了嵌入式設備和某些超級計算機意外,我們使用 虛擬尋址來取代物理尋址。
使用虛擬尋址,CPU通過生成一個虛擬地址(VIrtual Address)來訪問主存,這個虛擬地址在被送到內存前先轉換成適當的物理地址。將虛擬地址轉換成物理地址的任務叫做地址翻譯。原理 如下圖:
注:MMU(Memory Management Unit,內存管理單元),CPU上的一個專用硬件,用來存放在主存中的查詢表來動態翻譯虛擬地址。
地址空間
地址空間是一個線性的非負整數地址的有序集合:
如果像是{0,1,2,……}一樣,我們可以稱之為線性地址空間。
分為虛擬地址空間和物理地址空間,分別對應虛擬內存和物理內存。
地址空間幫助我們區分了數據對象(字節)和它們的屬性(地址)。主存中的每字節都有一個選自虛擬空間的虛擬地址和一個選自物理空間的物理地址。
頁
(注意,這里只講了頁式虛擬內存,還有另外一種段式虛擬內存,也可以把頁式當成一種特殊的段式)
現代操作系統將內存劃分為頁,來簡化內存管理,一個頁其實就是一段連續的內存地址的集合,通常有 4k 和 16k(iOS 64 位是 16K)的,成為 Virtual Page 虛擬頁。與之對應的物理內存被稱為 Physical Page 物理頁。
注意 虛擬頁的個數可能和物理頁個數不一樣 比如說一個 64 位操作系統中使用 48 位地址空間的 虛擬頁大小為 16K,那么其虛擬頁可數可達到(2^48 / 2^14 = 16M 個)假設物理內存只有 4G 那么物理頁可能只有 (2^32 / 2^14 = 256k 個)。
任何時刻,虛擬頁面的集合都分為三個不想交的子集:
- 未分配的:VM系統還未分配的(或者創建)的頁。未分配的塊沒有任何數據和它們相關聯,因此也就不占用任何磁盤空間。
- 緩存的:當前已緩存在物理內存中的已分配頁。
- 未緩存的:未緩存在物理內存中的已分配頁。
DRAM中的結構
我們用SRAM(靜態RAM)來表示L1、L2和L3高速緩存,用DRAM(動態RAM)表示虛擬內存中的緩存(它在主頁中緩存虛擬頁)。
在緩存中,DRAM未命中比SRAM要昂貴的多,因為SRAM未命中可以有DRAM來兜底,DRAM未命中就需要用磁盤來兜底(磁盤要比DRAM慢100000多倍,而且從磁盤的一個扇區讀取第一個字節的時間開銷比讀連續的字節要慢非常非常多)。因為上面的原因,虛擬頁往往設置的比較大,通常4KB-2MB。而且,DRAM是全相聯的, 任何虛擬頁都可以放置在任何物理頁之中。
頁表
操作系統使用頁表(PageTable),將虛擬頁映射到物理頁。每次地址翻譯硬件將一個虛擬地址轉換為物理地址時,都會讀取頁表。
頁表實際上是一個頁表條目(Page Table Entry,PTE)的數組。虛擬地址空間中的每個頁在頁表中一個固定偏移處都有一個PTE。結構如下:
缺頁
假如DRAM緩存未命中,被稱之為缺頁(page fault)。當缺頁發生時,會啟動內核中的缺頁異常程序,選擇一個犧牲頁,進行磁盤和內存中數據的交換。
在iOS系統中,因為內存的緊張,并未采用這種方式,而是類似OOM警告的方式來控制,MacOS中是存在的。
虛擬內存的內存管理
VM為每個進程都提供了一個獨立的頁表,因而也就是一個獨立的虛擬地址空間。使用VM也會有很多優點
- 簡化鏈接
每個獨立的地址空間允許每個進程的內存映像使用相同的基本格式,而不管代碼和數據實際存放在物理內存的何處。- 簡化加載
VM使得容易向內存中加載可執行文件和共享對象文件。- 簡化共享
獨立的地址空間為操作系統提供了一個管理用戶進程和操作系統自身之間共享的一致機制。- 簡化內存分配
假如遇到需要共享內存數據的時候,VM機制可以幫助我們有選擇的訪問共享頁面。
內存保護
我們應該明白,不應該允許一個用戶進程任意修改它的只讀代碼段;不允許修改內核的代碼和數據結構;不允許讀寫其他進程的私有內存。
為了提供這種保護,地址翻譯機制會在讀取PTE的時候,添加一些額外的許可位來控制虛擬頁面。如下圖所示:
地址翻譯
n位的虛擬地址包含兩個部分:p位的虛擬頁面偏移(VPO)和一個(n-p)位的虛擬頁號(VPN)。MMU使用VPN來選擇適當的PTE。
這里詳細細節查看《深入理解計算機系統》(第三版)p568-p570
TLB
每次CPU產生一個地址,MMU就必須查閱一個PTE,以便將虛擬地址翻譯成為物理地址。這會造成非常大的性能損耗。如何解決呢?答案簡單,使用緩存!我們在這里使用一個叫做 翻譯后備緩沖器(Transalation Lookaside Buffer-TLB)的東西來幫助我們處理。當TLB未命中時,MMU再去L1緩存中獲取對應的PTE,然后再將它放到TLB中。
多級頁表
一般來說,系統的地址空間也是有限的,我們不能每次都要一起訪問整個頁表。這里我們可以使用 多級頁表技術。
一級頁表對應二級頁表,二級頁表對應虛擬內存頁面。我們只要把一級頁表一直放到主存中就好了,需要的時候再去訪問二級頁表。
Linux中的虛擬內存
操作系統為每個進程維護一個單獨的虛擬地址空間,分為兩部分。
- 內核虛擬內存
包含內核中的代碼和數據結構,還有一些被映射到所有進程共享的內存頁面。還有一些頁表,內核在進程上下文中執行代碼使用的棧。- 進程虛擬內存
OS 將內存組織為一些區域(Sement)的集合,代碼端,數據端,共享庫端,線程棧都是不同的區域,分段的原因是便于管理內存的權限,如果了解過 Mach-O 文件或者 ELF 文件的讀者可以看到相同的 Segment 里面的內存權限是相同的,每個 Segment 再劃分不同的內容為 section。
內存映射 (mmap)
在Linux中,通過將一個虛擬內存區域與一個磁盤上的對象關聯起來,以初始化這個虛擬內存區域的內容。
大致過程如下:進程先在虛擬地址空間中創建虛擬映射區域,然后內核開始調用mmap函數,實現物理地址和虛擬地址的映射。
實現細節可以查看 《深入理解計算機系統》(第三版)p582-p586
我們需要記住:mmap為共享書、創建新的進程以及加載程序提供了一個高效的機制。應用可以使用mmap函數來手工德創建和刪除虛擬地址空間區域內一個稱謂堆(heap)的區域。
而mmap在iOS的用處:
- mmap 讓讀寫一個文件像操作一個內存地址一樣簡單方便,
- mmap 效率極高,不用將一個內容從磁盤讀入內核態再拷貝至用戶態
- mmap映射的文件由操作系統接管,如果進程 Crash 操作系統會保證文件刷新回磁盤。
通過以上的特點,我們可以在圖片加載(例如FastImageCache),數據存儲以及關鍵的crash收集上報中使用。
動態內存分配
在運行時需要額外的虛擬內存的時候,用動態內存分配器更方便、更好的可移植性。
動態內存分配器維護著一個進程的虛擬內存區域,稱之為 堆。堆可以被視為一組大小不同的塊(block)的集合。這些塊要不然就是分配的,要不然就是空閑的。
分配器有兩種基本風格,兩種風格都要求顯式的分配塊:
- 顯式分配器 (手動管理內存,嚴格來講ARC算是這個的變種)
- 隱式分配器 (垃圾收集,Java等語言采用這種)
顯示分配器的實現細節可以查看 《深入理解計算機系統》(第三版)p587-p605。十分推薦iOS去讀,很多時候跳出來看一下原理,會讓自己有新的認知。
隱式分配器或者說垃圾收集實現細節可以查看 《深入理解計算機系統》(第三版)p605-p609。
因為我對使用GC的語言的語言沒什么研究,兩者的區別優劣我無法給出,不過推薦一下這篇文章Garbage Collection vs Automatic Reference Counting。
傾寒推薦這個代碼C + + 實現一個簡易的內存池分配器,也可以看一下。
iOS Memory
在上邊我們了解的頁這一概念,iOS實際上也是使用了這一概念。
我們使用以下代碼來查看數據
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "mach/mach.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
printf("page-size:%ld \nmask:%ld\nshift:%d \n", vm_kernel_page_size, vm_kernel_page_mask, vm_kernel_page_shift);
printf("%ld\n", sysconf(_SC_PAGE_SIZE));
printf("%d\n", getpagesize());
printf("%lu\n", PAGE_SIZE);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
得到的數據為(iPhone 8 plus和iPhone SE):
page-size:16384
mask:16383
shift:14
16384
16384
16384 = 16kb
我們用過以上數據可以得知:頁大小為16kb,虛擬地址偏移為14。這里即使用上文中提過的TLB來翻譯地址。
在我們未寫入數據,但是剛剛被操作系統分配或者磁盤映射的時候,內存都是出于 Clean的狀態,但是一旦被寫入了沒操作系統會將它標記為 Dirty。
- Clean:指的是能夠被系統清理出內存并且在有需要的時候重新加載的數據,包括:Memory mapped files;Frameworks中的__DATA_CONST部分;應用的二進制可執行文件。
- Dirty:指的是不能被系統回收的內存占用,包括:堆上的對象;圖片解碼數據;Frameworks中的__DATA和__DATA_DIRTY部分。
標記的一個好處在于:因為Dirty頁面已經被寫入數據,是要比Clean重要的多的。當操作系統發現內存十分緊張的時候,會嘗試驅逐一部分內存頁面。Clean的頁面會因為優先級的原因被首先驅逐,并開始和磁盤(中的backing store部分)交換分區,等到需要使用的時候再去讀取。
但是我們這里要注意一點,iOS因為是在移動端使用,移動端使用的是閃存。現在新版的iPhone使用的都是TLC(Triple-Level Cell),過于頻繁的讀寫會嚴重影響閃存的使用壽命(實際上是二氧化硅薄膜因為電子的頻繁進出而變薄)。所以并沒有使用上邊這個磁盤交換機制,因此如果出現內存緊張的情況,iOS會使用Compressed Memory機制。在內存緊張的時候,將不常使用的內存壓縮并且在需要的時候解壓。
When your system’s memory begins to fill up, Compressed Memory automatically compresses the least recently used items in memory, compacting them to about half their original size. When these items are needed again, they can be instantly uncompressed.
這個舉措,特點可以歸納為:
- 減少了不活躍內存占用
- 改善了電源效率,通過壓縮減少磁盤IO帶來的損耗
- 壓縮/解壓十分迅速,能夠盡可能減少 CPU 的時間開銷
- 支持多核操作
從某種意義上來說,我們可以把Compressed memory視為Dirty memory。
memory footprint = dirty size + compressed size ,這也就是我們需要并且能夠嘗試去減少的內存占用
當我們的app的memory footprint 達到一定的值時,我們會受到內存警告(Memory Warnings)。
如果我們收到了內存警告,系統本身會釋放一部分內存頁面(例如NSCache機制),但是也會向當前運行的程序發送低內存警告,我們也要對此作出相應。
UIKit中有幾種接受低內存警告的方法:
- applicationDidReceiveMemoryWarning:方法;
- 在UIViewController中重寫didReceiveMemoryWarning;
- 注冊接受UIApplicationDidReceiveMemoryWarningNotification通知
如果我們對此置之不理,程序有可能直接被干掉,那時候我們就會陷入OOM的困境之中。
監測內存的工具
Xcode
命令行工具暫且不提,那套更加適合MacOS。
在Xcode中,我們可以使用三種工具來測量內存:
- Xcode memory gauge
- Instruments(主要是Leaks、Allocation、Counters以及System Trace中的Virtual Memory Trace)
- Xcode Memory Debugger
在Xcode10之后,當內存過大的時候,也會觸發debugger,自動捕獲 EXC_RESOURCE RESOURCE_TYPE_MEMORY
異常,并自動斷點在出問題的地方。
在在Product->Scheme->Edit Scheme->Diagnostics中,開啟 Malloc Stack 功能,建議使用Live Allocations Only選項。
中開啟Malloc Stack功能,使用 Live Allocations Only選項,會在lldb中記錄調試過程中對象創建的堆棧,配合使用 malloc_history
工具,可以方便我們定位到占用過大內存的對象的創建位置。
代碼方法
獲取應用使用真實物理內存值的代碼:
- (NSUInteger)getResidentMemory
{
struct mach_task_basic_info info;
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
int r = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)& info, & count);
if (r == KERN_SUCCESS)
{
return info.resident_size;
}
else
{
return -1;
}
}
線上內存檢測工具
1.MLeaksFinder
2.FBRetainCycleDetector
3.OOMDetector
當然,我們也可以自己在理解內存檢測的原理之后,自己去實現一些輪子,以更加貼合自己的使用場景。
如何注意內存優化
0.多用懶加載
1.weak替代 unsafe_unretain ,以及注意assign;
2.安全的使用weak;
3.autoreleasepool多用;
4.對UI、動畫機制深入了解,尤其是動畫以及Cell復用機制;
5.imageName:
5.performSelect謹慎使用;
6.倒計時使用注意,設計一定要嚴謹;
7.多使用Cache而非dictionary;
8.監測性能組件使用mmap存放讀取數據;
9.NSDateFormate注意;
10.謹慎小心的使用指針,小心野指針;
11.WKWebView 是跨進程通信的,不會占用我們的 APP 使用的物理內存量,但是依然要小心謹慎的測量;
12.在保證安全的前提下,選用一些更小的數據結構;
13.對待大的貼圖要謹慎使用;
14.謹慎小心的使用指針;
15.注意 NSDateFormatter的使用。
后續記錄計劃
SLC/MLC/TLC對比,為什么選用TLC。
OOM是個什么鬼。
如何設計一個一個內存監測組件。
如何注意內存優化
中各項的解釋。
參考和鳴謝
《程序員的自我修養-鏈接、裝載和庫》第一版(第十章)
《深入理解計算機系統》第三版(第九章)
《iOS 和 macOS 性能優化:Cocoa、Cocoa Touch、Objective-C 和 Swift》第一版(第五章)
《高性能iOS應用開發》 第一版-第二章
《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》第五十條
WWDC 2018-Session 416: iOS Memory Deep Dive
iOS Memory Deep Dive
OS X Mavericks Core Technology Overview
Memory Usage Performance Guidelines
Instruments Help
iOS-Monitor-Platform
感謝傾寒、冬瓜在創作中給予的幫助。
結語
iOS的APM是真的難!
寫了這么多越來越覺得自己蠢!!!!!!!
還是要多看書!
投簡歷是真的難!