15 - iOS的內(nèi)存認識

OC底層原理探索文檔匯總

主要內(nèi)容:

1、內(nèi)存的認識
2、棧區(qū)和堆區(qū)的使用驗證
3、內(nèi)存泄漏和內(nèi)存溢出

內(nèi)存的認識

我們所說的內(nèi)存其實準確的說是虛擬內(nèi)存,不是物理內(nèi)存,由多張頁組成。
分成內(nèi)核區(qū)和數(shù)據(jù)區(qū),其中數(shù)據(jù)區(qū)包括五大區(qū)以及保留區(qū)。

  • 內(nèi)核區(qū)是系統(tǒng)進行內(nèi)核處理操作的區(qū)域
  • 五大區(qū)就是我們常說的棧、堆、全局區(qū)、常量區(qū)、代碼區(qū)。
  • 保留區(qū)是預(yù)留給系統(tǒng)處理nil等的區(qū)域

以4G手機為例,1個G用來存放內(nèi)核區(qū),3個G用來存放數(shù)據(jù)區(qū)。

我們所涉及的其實就是五大區(qū),通常使用的是棧和堆。這兩個要著重了解。

內(nèi)存布局.jpg

五大區(qū)的認識

內(nèi)存五大區(qū)圖示.png

介紹: 棧是從高地址向低地址擴展的一段連續(xù)內(nèi)存空間,遵循先進后出原則

存儲:

  • 用來存放局部變量(函數(shù)參數(shù))
  • 系統(tǒng)會自動分配和釋放,隨著方法的創(chuàng)建而創(chuàng)建,方法的銷毀而銷毀
  • 一般在運行時分配
  • 在iOS中是以0X7開頭的內(nèi)存空間

優(yōu)缺點:

優(yōu)點: 棧是由編譯器自動分配并釋放的,不會產(chǎn)生內(nèi)存碎片,所以快速高效
缺點: 內(nèi)存大小有限制,數(shù)據(jù)不靈活

注意:

  • 棧是系統(tǒng)數(shù)據(jù)結(jié)構(gòu),對應(yīng)的線程或進程是唯一的,也就是一個棧不能被多個線程或進程共享
  • 棧是從高地址向低地址擴展,也就是后添加的是低地址,先添加的在高地址,也就是后添加的是低地址,先添加的在高地址

棧為什么需要由高地址向低地址擴展?

  • 這樣設(shè)計可以使得堆和棧能夠充分利用空閑的地址空間,因為棧和堆的空間大小會隨著運行的過程不斷變化,所以無法界定棧和堆的分割線。無法確定給堆分配多少空間。
  • 所以棧和堆就分別從兩種向中間擴展內(nèi)存??梢宰畲笙薅鹊睦檬S嗟牡刂房臻g

介紹:

  • 堆是從低地址向高地址擴展的不連續(xù)的內(nèi)存區(qū)域,通過指針來查找數(shù)據(jù),遵循先進先出原則
  • 分配堆的空間大小不確定,而且它的生命周期時間不確定,所以需要人為管理。
  • 內(nèi)存管理基本上可以說是管理堆,在iOS中是通過引用計數(shù)來管理的

存儲:

  • 堆區(qū)是由程序員的動態(tài)分配和釋放的,堆區(qū)的分配一般是在運行時分配
  • 用來存儲引用類型數(shù)據(jù)
    • 用于存放OC中alloc或者new開辟空間創(chuàng)造的對象
    • 用于存放C語言中用malloc/calloc/realloc分配的空間,需要free釋放
  • 地址空間以0X6開頭,空間的分配總是動態(tài)的

優(yōu)缺點:

優(yōu)點: 靈活方便,數(shù)據(jù)適應(yīng)面廣泛,容量大
缺點: 需手動管理,速度慢、容易產(chǎn)生內(nèi)存碎片

全局區(qū)

介紹: 全局區(qū)是編譯時分配的內(nèi)存空間,用來存儲全局變量和靜態(tài)變量,包括BSS區(qū)和data區(qū)

存儲:
BSS區(qū):

  • 存放未初始化的全局變量和靜態(tài)變量
  • 初始化后回收

data區(qū):

  • 存放已初始化的全局變量和靜態(tài)變量
  • 程序結(jié)束時系統(tǒng)回收

常量區(qū)

介紹: 常量區(qū)存儲常量

注意:

  • 常量區(qū)是編譯時分配的內(nèi)存空間
  • 程序結(jié)束后由系統(tǒng)釋放

代碼區(qū)

介紹:

  • 代碼區(qū)是編譯時分配主要用于存放程序運行時的代碼,代碼會被編譯成二進制存進內(nèi)存的
  • 也就是已經(jīng)被加載的類、常量,被編譯后的二進制代碼

存儲區(qū)的驗證

int quanju;
- (void)test{
    //棧
    NSInteger i = 123;
    NSLog(@"i的內(nèi)存地址:%p", &i);
    
    //堆
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"obj的內(nèi)存地址:%p", obj);
    NSLog(@"&obj的內(nèi)存地址:%p", &obj);
    
    //靜態(tài)
    static NSInteger jingtai = 111;
    NSLog(@"jingtai的內(nèi)存地址:%p", &jingtai);
    
    //全局區(qū)
    quanju = 222;
    NSLog(@"quanju的內(nèi)存地址:%p", &quanju);
    
    //常量池
    NSString *string = @"CJL";
    NSLog(@"string的內(nèi)存地址:%p", string);
    NSLog(@"&string的內(nèi)存地址:%p", &string);
}

運行結(jié)果:

2021-11-09 22:23:24.958924+0800 內(nèi)存管理[76267:847507] i的內(nèi)存地址:0x7ff7b233ef18
2021-11-09 22:23:24.959026+0800 內(nèi)存管理[76267:847507] obj的內(nèi)存地址:0x600001b9c3b0
2021-11-09 22:23:24.959099+0800 內(nèi)存管理[76267:847507] &obj的內(nèi)存地址:0x7ff7b233ef10
2021-11-09 22:23:24.959180+0800 內(nèi)存管理[76267:847507] jingtai的內(nèi)存地址:0x10dbc6520
2021-11-09 22:23:24.959249+0800 內(nèi)存管理[76267:847507] quanju的內(nèi)存地址:0x10dbc66a8
2021-11-09 22:23:24.959315+0800 內(nèi)存管理[76267:847507] string的內(nèi)存地址:0x10dbc10c0
2021-11-09 22:23:24.959377+0800 內(nèi)存管理[76267:847507] &string的內(nèi)存地址:0x7ff7b233ef08

說明:

  • 從中可以看出在棧中的數(shù)據(jù)是以0x7開頭的
  • 堆的數(shù)據(jù)是以0x6開頭的
  • 常量區(qū)、靜態(tài)區(qū)、全局區(qū)的數(shù)據(jù)是以0x10開頭的

棧區(qū)的使用驗證

代碼:

void baseDataTypeTest(){
    int a = 9;
    int b = 10;
    int *f = &a;
    
    NSLog(@"基本數(shù)據(jù)類型的變量a:%d -- 變量地址:%p",a,&a);
    NSLog(@"基本數(shù)據(jù)類型的變量b:%d -- 變量地址:%p",b,&b);
    NSLog(@"基本數(shù)據(jù)類型的指針a:%p -- 指針b:%p",f,f-1);
    NSLog(@"指針變量f所指向的a變量的內(nèi)容:%d -- 變量b的內(nèi)容:%d",*f,*(f-1));
    NSLog(@"指針變量f所在的內(nèi)存地址:%p",&f);
}

結(jié)果:

 2021-10-12 12:29:14.880387+0800 指針偏移[70684:2791244] 基本數(shù)據(jù)類型的變量a:9 -- 變量地址:0x1040bb2bc基本類型都是在
 2021-10-12 12:29:14.880773+0800 指針偏移[70684:2791244] 基本數(shù)據(jù)類型的變量b:10 -- 變量地址:0x1040bb2b8
 2021-10-12 12:29:14.880818+0800 指針偏移[70684:2791244] 基本數(shù)據(jù)類型的指針a:0x1040bb2bc -- 指針b:0x1040bb2b8
 2021-10-12 12:29:14.880848+0800 指針偏移[70684:2791244] 指針變量f所指向的a變量的內(nèi)容:9 -- 變量b的內(nèi)容:10
 2021-10-12 12:29:14.880872+0800 指針偏移[70684:2791244] 指針變量f所在的內(nèi)存地址:0x1040bb2b0

分析:

  • 1、基本數(shù)據(jù)類型存儲在棧中,因此直接將10數(shù)值存儲在棧中
  • 2、棧的存儲方式是從高地址到低地址,所以變量a的地址值比變量b的地址值要高
  • 3、因為類型為int型,a占有4個字節(jié),所以b地址比a地址低了4個字節(jié)
  • 4、&a是將變量a的地址取出來
  • 5、int *f = &a是將變量a的地址賦值到指針變量e上
  • 6、打印f和f-1可以看到分別打印的是a和b的地址,也可以證明指針變量存儲的就是地址
  • 7、為什么是f-1,而不是f+1,這是因為棧的存儲方式是從高地址到低地址,所以得到b的指針需要通過f-1
  • 8、*f是對指針變量使用 *,可以得到指針變量存儲的地址所存儲的內(nèi)容,因此 * f, *(f-1)分別到的a和b變量的內(nèi)容
  • 9、也可以對指針變量f取地址,取到的地址就是指針變量的地址,被b地址小了8個字節(jié)
棧的認識.png

堆區(qū)的使用驗證

代碼:

void quoteTest(){
    NSperson *person1 = [NSperson alloc];
    NSperson *person2 = [NSperson alloc];
    NSLog(@"引用類型的指針person1:%@ -- 變量地址:%p",person1,&person1);
    NSLog(@"引用類型的指針person2:%@ -- 變量地址:%p",person2,&person2);
}

運行結(jié)果:

 2021-10-12 12:29:14.881025+0800 指針偏移[70684:2791244] 引用類型的指針person1:<NSperson: 0x105014c50> -- 變量地址:0x1040bb2b8
 2021-10-12 12:29:14.881069+0800 指針偏移[70684:2791244] 引用類型的指針person2:<NSperson: 0x105013d90> -- 變量地址:0x1040bb2b0

分析:

  • 指針是在棧中,對象是在堆中
  • 一個指針占8個字節(jié),所以指針按地址空間來說&p1和&p2二者之間相差8個字節(jié)
  • 而指針本身是緊挨著的,也就是說&p1的下一個指針就是&p2

圖示:

引用類型的指針偏移.png

堆棧溢出的認識

溢出原因

一般情況下應(yīng)用程序是不需要考慮堆和棧的大小的,但是事實上堆和棧都不是無上限的。當我們寫入的數(shù)據(jù)超出了分配的內(nèi)存空間,就會造成溢出。

  • 過多的遞歸會導致棧溢出。
  • 過多的alloc變量會導致堆溢出。

預(yù)防方法

棧:

  • 避免層次過深的遞歸調(diào)用;
  • 一個函數(shù)內(nèi)不要使用過多的局部變量,控制局部變量的大小

堆:

  • 避免分配占用空間太大的對象
  • 對象要及時釋放
  • 實在不行,適當?shù)那榫跋抡{(diào)用系統(tǒng)API修改線程的堆棧大小,可以改大;

堆棧泄漏的認識

內(nèi)存泄漏通俗來說:本該回收的對象沒有及時回收,無法分配給別人,相當于失去了這一部分內(nèi)存。

原因可能有這幾種:

  • 創(chuàng)建大量對象后沒有去釋放
  • 循環(huán)引用,對象無法釋放

總結(jié)

  • 我們的內(nèi)存分為內(nèi)核區(qū)和數(shù)據(jù)區(qū),數(shù)據(jù)區(qū)有五大區(qū)和保留區(qū)
  • 我們通常所說的內(nèi)存管理其實就是指管理堆,因為棧會很快被釋放掉,全局區(qū)、常量池、代碼區(qū)都是系統(tǒng)控制的
  • 全局區(qū)是可讀可寫的,編譯時分配好內(nèi)存空間后還可以動態(tài)的寫入,
  • 常量區(qū)和方法區(qū)都是只讀的,在編譯時就已經(jīng)分配好后不可再動態(tài)寫入
  • 避免堆棧溢出
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容