在iOS中,內存主要分為棧區、堆區、全局區、常量區、代碼區
五大區域。如下圖所示
棧區(Stack)
定義
棧是
系統數據結構
,其對應的進程或者線程是唯一
的棧是
由高地址向低地址擴展
的數據結構棧是一塊
連續的內存區域
,遵循先進后出(FILO)
原則棧的地址空間在iOS中是
以0x7開頭
棧區的內存一般
在運行時分配
存儲
棧區是由編譯器自動分配并釋放的
,主要用來存儲
局部變量
函數的參數
,例如函數的隱藏參數(id self,SEL _cmd)
優缺點
優點:因為棧是
由編譯器自動分配并釋放
的,不會產生內存碎片,所以快速高效
-
缺點:棧的
內存大小有限制
,數據不靈活
iOS主線程棧大小是
1MB
其他線程是
512KB
MAC只有
8M
以上內存大小的說明,在Threading Programming Guide中有相關說明
堆區(Heap)
定義
堆是
由低地址向高地址擴展
的數據結構堆是
不連續的內存區域
,類似于鏈表結構(便于增刪,不便于查詢),遵循先進先出(FIFO)
原則堆的地址空間在iOS中是以
0x6開頭
,其空間的分配總是動態
的堆區內存的分配一般是
在運行時分配
存儲
堆區是由程序員動態分配和釋放的
,如果程序員不釋放,程序結束后,可能由操作系統回收,主要用于存放
OC
中使用alloc
或者 使用new
開辟空間創建對象C
語言中使用malloc
、calloc
、realloc
分配的空間,需要free
釋放
優缺點
優點:靈活方便,數據適應面廣泛
缺點:需
手動管理
,速度慢、容易產生內存碎片
當需要訪問堆中內存時,一般需要先通過對象讀取到棧區的指針地址
,然后通過指針地址訪問堆區
全局區(靜態區,即.bss & .data)
全局區是編譯時分配
的內存空間,在iOS中一般以0x1開頭
,在程序運行過程中,此內存中的數據一直存在,程序結束后由系統釋放
,主要存放
未初始化
的全局變量
和靜態變量
,即BSS區(.bss)
已初始化
的全局變量
和靜態變量
,即數據區(.data)
其中,全局變量是指變量值可以在運行時被動態修改,而靜態變量是static修飾的變量,包含靜態局部變量和靜態全局變量
常量區(即.rodata)
常量區是編譯時分配
的內存空間,在程序結束后由系統釋放
,主要存放
- 已經使用了的,且沒有指向的
字符串常量
字符串常量因為可能在程序中被多次使用,所以在程序運行之前
就會提前分配內存
代碼區(即.text)
代碼區是編譯時分配
主要用于存放程序運行時的代碼
,代碼會被編譯成二進制存進內存
的
驗證內存五大區域
運行下面一段代碼,看看變量在內存中是如何分配的
- (void)test{
NSInteger i = 123;
NSLog(@"i的內存地址:%p", &i);
NSString *string = @"CJL";
NSLog(@"string的內存地址:%p", string);
NSLog(@"&string的內存地址:%p", &string);
NSObject *obj = [[NSObject alloc] init];
NSLog(@"obj的內存地址:%p", obj);
NSLog(@"&obj的內存地址:%p", &obj);
}
打印結果如下所示
2020-11-03 21:48:46.846481+0800 demo[3786:131132] i的內存地址:0x7ffeefbff498
2020-11-03 21:48:46.846554+0800 demo[3786:131132] string的內存地址:0x100004050
2020-11-03 21:48:46.846600+0800 demo[3786:131132] &string的內存地址:0x7ffeefbff490
2020-11-03 21:48:46.846648+0800 demo[3786:131132] obj的內存地址:0x100434b00
2020-11-03 21:48:46.846689+0800 demo[3786:131132] &obj的內存地址:0x7ffeefbff488
對于
局部變量i
,從地址可以看出是0x7開頭
,所以i存放在棧區
-
對于
字符串對象string
,分別打印了string的對象地址
和string對象的指針地址
string的
對象地址
是以0x1開頭
,說明是存放在常量區
string
對象的指針地址
是以0x7開頭
,說明是存放在棧區
-
對于
alloc創建的對象ob
j,分別打印了obj的對象地址
和obj對象的指針地址
obj的
對象地址
是以0x6開頭
,說明是存放在堆區
obj
對象的指針地址
是以0x7開頭
,說明是存放在棧區
函數棧
函數棧
又稱為棧區
,在內存中從高地址往低地址分配,與堆區相對,具體圖示請查看文章最開始的圖示棧幀
是指函數(運行中且未完成)占用的一塊獨立的連續內存區域
應用中新創建的
每個線程都有專用的棧空間
,棧可以在線程期間自由使用。而線程中有千千萬萬的函數調用,這些函數共享
進程的這個棧空間
。每個函數所使用的棧空間是一個棧幀,所有的棧幀就組成了這個線程完整的棧
函數調用是發生在棧上
的,每個函數的相關信息
(例如局部變量、調用記錄等)都存儲在一個棧幀中
,每執行一次函數調用
,就會生成一個與其相關的棧幀,然后將其棧幀壓入函數棧
,而當函數執行結束
,則將此函數對應的棧幀出棧并釋放掉
如下圖所示,是經典圖 - ARM的棧幀布局方式
其中
main stack frame
為調用函數的棧幀
func1 stack frame
為當前函數(被調用者)的棧幀
棧底
在高
地址,棧向下增長。FP
就是棧基址
,它指向函數的棧幀起始地址
SP
則是函數的棧指針
,它指向棧頂的位置
。ARM壓棧
的順序
很是規矩(也比較容易被黑客攻破么),依次為當前函數指針PC
、返回指針LR
、棧指針SP
、棧基址FP
、傳入參數個數及指針
、本地變量
和臨時變量
。如果函數準備調用另一個函數,跳轉之前臨時變量區先要保存另一個函數的參數。ARM也可以
用棧基址和棧指針明確標示棧幀的位置
,棧指針SP一直移動,ARM的特點
是,兩個棧空間內的地址(SP+FP)前面,必然有兩個代碼地址(PC+LR)明確標示著調用函數位置內的某個地址。
堆棧溢出
一般情況下應用程序是不需要考慮堆和棧的大小的,但是事實上堆和棧都不是無上限的,過多的遞歸會導致棧溢出,過多的alloc變量會導致堆溢出。
所以預防堆棧溢出
的方法:
避免層次過深
的遞歸
調用不要使用過多的局部變量
,控制局部變量的大小避免分配
占用空間太大的對象
,并及時釋放
實在不行,適當的情景下
調用系統API修改線程的堆棧大小
棧幀示例
棧幀程序示例
int Add(int x,int y) {
int z = 0;
z = x + y;
return z;
}
int main() {
int a = 10;
int b = 20;
int ret = Add(a, b);
}
程序執行時棧區中棧幀的變化如下圖所示