Rust語(yǔ)言學(xué)習(xí)之內(nèi)存模型分析
## 背景
隨著Rust越來(lái)越多的應(yīng)用良好表現(xiàn),吸引了越來(lái)越多的開(kāi)發(fā)者關(guān)注和各領(lǐng)域?qū)?yīng)的Rust解決方案出現(xiàn)。對(duì)于從C/GO/Java/Python這些語(yǔ)言開(kāi)發(fā)者來(lái)說(shuō),學(xué)習(xí)Rust語(yǔ)言最大的挑戰(zhàn)就是需要理解Rust語(yǔ)言的內(nèi)存管理模型。而Rust開(kāi)創(chuàng)性的所有權(quán)管理機(jī)制,是我們理解和精通該門(mén)語(yǔ)言必須要首先弄清楚的要點(diǎn)。而這也是為什么大家一致認(rèn)為Rust的學(xué)習(xí)曲線陡峭的最核心原因。
>本系列文章主要是閱讀圖靈系列叢書(shū)《Rust程序設(shè)計(jì)》讀書(shū)筆記,再以有經(jīng)驗(yàn)程序員新學(xué)Rust語(yǔ)言的路線來(lái)編寫(xiě)。
## 示例
我們首先來(lái)看一段代碼:
這段代碼邏輯很簡(jiǎn)單,僅僅是定義了三個(gè)變量,并做一些賦值和打印操作。三個(gè)變量定義如下:
1.變量v,定義v為一個(gè)向量,其內(nèi)包含了6個(gè)Int類(lèi)型數(shù)字。
2.變量r,再將v的地址賦值給變量r,相當(dāng)于r是v向量變量的引用。
3.變量aside,最后將變量v賦值給變量aside。
最后打印r變量的第0個(gè)元素。
如果根據(jù)我們以往的編程經(jīng)驗(yàn),該段代碼找不出來(lái)任何邏輯和寫(xiě)法問(wèn)題,但是在Rust中,編譯運(yùn)行該段代碼,其會(huì)報(bào)錯(cuò):
從這段錯(cuò)誤,我們可以得出以下結(jié)論:
1.Rust能夠在編譯階段發(fā)現(xiàn)代碼runtime階段內(nèi)存錯(cuò)誤問(wèn)題。
2.Rust語(yǔ)言編譯時(shí)能夠非常詳盡的解釋編譯錯(cuò)誤。
3.編譯器提示在將v的引用賦值給r時(shí),相當(dāng)于將v借用給了r,借用完成后,再次將v賦值給aside操作時(shí),所有權(quán)出現(xiàn)了Move操作,v的所有權(quán)到了變量aside之上,這個(gè)時(shí)候,再去訪問(wèn)r變量,就會(huì)出現(xiàn)報(bào)錯(cuò),相當(dāng)于訪問(wèn)了一個(gè)懸空指針。
而這個(gè)報(bào)錯(cuò),正是由于Rust的所有權(quán)機(jī)制導(dǎo)致的。那么,我們這篇文章主要是為了解釋下面兩個(gè)問(wèn)題:
1.Rust所有權(quán)制度到底是為了解決什么問(wèn)題出現(xiàn)的?
2.Rust所有權(quán)制度如何解決該問(wèn)題的?
## Rust內(nèi)存模型
在我們過(guò)往的編程語(yǔ)言使用經(jīng)驗(yàn)中,我們都了解類(lèi)似C/C++語(yǔ)言,內(nèi)存管理都是靠程序員手動(dòng)來(lái)維護(hù)的,new/free的操作都是程序員自己去控制,其性能非常高,但是由此引入了非常多的內(nèi)存問(wèn)題,比如訪問(wèn)了已經(jīng)釋放了的內(nèi)存地址,或者內(nèi)存沒(méi)有是否導(dǎo)致內(nèi)存泄漏從而系統(tǒng)不穩(wěn)定等等問(wèn)題。為了最大限度避免這些問(wèn)題,在使用C/C++語(yǔ)言時(shí),大多數(shù)程序員都要手寫(xiě)一個(gè)內(nèi)存池來(lái)進(jìn)行內(nèi)存管理,而由此帶來(lái)的內(nèi)存碎片等問(wèn)題都不容易處理。而Java類(lèi)的,內(nèi)存管理都由Jvm虛擬機(jī)來(lái)完成,減少了程序員的出錯(cuò),但是Jvm虛擬機(jī)進(jìn)行自動(dòng)內(nèi)存整理(GC)時(shí),又帶來(lái)了很大的CPU波動(dòng)。影響了業(yè)務(wù)系統(tǒng),導(dǎo)致業(yè)務(wù)系統(tǒng)在高性能計(jì)算時(shí)由于GC出現(xiàn)而出現(xiàn)大幅度時(shí)延。
而Rust即想得到C/C++手動(dòng)管理內(nèi)存的性能,又想自動(dòng)化去管理內(nèi)存,還要避免類(lèi)似Java統(tǒng)一GC的性能損失。不得不說(shuō),這種成年人的選擇總會(huì)讓人為之精神一振。在此背景下,Rust所有權(quán)系統(tǒng)應(yīng)運(yùn)而生,做到了熊掌與魚(yú)兼得,后續(xù),我們主要分析下Rust所有權(quán)系統(tǒng)是如何工作的。
## Rust內(nèi)存結(jié)構(gòu)示例
Rust所有權(quán)系統(tǒng)主要是用來(lái)做自動(dòng)化內(nèi)存管理,那么,我們首先分析下Rust代碼內(nèi)存結(jié)構(gòu)。其示意圖如下所示:

和C語(yǔ)言類(lèi)似,Rust程序內(nèi)存布局包括了堆、棧、靜態(tài)數(shù)據(jù)區(qū)、只讀數(shù)據(jù)區(qū)和只讀代碼區(qū)。
其中,對(duì)于每個(gè)區(qū)存放的內(nèi)容,大抵可以如下分類(lèi):
1.棧:在編譯階段就可以確定哪些數(shù)據(jù)可以存放到棧上,由編譯器管理,函數(shù)局部變量等,存放到棧中。
2.堆:由程序員編寫(xiě)的代碼來(lái)申請(qǐng)使用,一般做大量數(shù)據(jù)讀寫(xiě)時(shí)使用,運(yùn)行時(shí)申請(qǐng)。
3.靜態(tài)數(shù)據(jù)區(qū):一般的靜態(tài)函數(shù)、靜態(tài)局部變量和靜態(tài)全局變量存放區(qū)域,在程序啟動(dòng)時(shí)初始化。
4.Literals(只讀數(shù)據(jù)區(qū)):存放代碼的文字常量區(qū)域。
5.Instructions(只讀代碼區(qū)):存放可執(zhí)行代碼區(qū)域。
我們以一段Rust代碼來(lái)距離,了解下Rust中各變量是如何存儲(chǔ)的,代碼如下:
該段代碼內(nèi)存布局如下圖所示:

分析內(nèi)存結(jié)構(gòu)圖,我們可以了解到下面結(jié)論:
1.noodles,oodles,poodles三個(gè)變量都存儲(chǔ)在棧上,并且都是三個(gè)胖指針。
2.noodles和oodles是指向同一塊內(nèi)存,只不過(guò)指針首地址不一樣。
3.poodles變量的數(shù)據(jù)內(nèi)容是存儲(chǔ)在預(yù)分配的只讀內(nèi)存區(qū)。
4.noodles的變量存儲(chǔ)格式包括三個(gè)部分,第一個(gè)部分是指向數(shù)據(jù)存儲(chǔ)堆內(nèi)存首地址,第二個(gè)部分是該變量的容量,第三個(gè)部分是該變量的長(zhǎng)度。(實(shí)際上Rust里String是用Vec來(lái)實(shí)現(xiàn)的,所以這里的容量是Vec管理策略來(lái)決定,Rust里分配原則是2->4->8,如果容量不夠,下次申請(qǐng)的為前一次的2倍。)
5.字符串常量poodles的內(nèi)存是提前分配好的只讀內(nèi)存區(qū)。
6.引用并不做深度拷貝操作,僅僅是指針指向數(shù)據(jù)堆內(nèi)存地址。
## 多語(yǔ)言?xún)?nèi)存賦值解析
內(nèi)存管理體現(xiàn)在每個(gè)語(yǔ)言對(duì)賦值操作的實(shí)現(xiàn)中,我們可以對(duì)比下Python、C++和Rust這幾種比較有代表性的語(yǔ)言,了解下他們各自對(duì)賦值操作內(nèi)存是如何管理的。
### Python
我們以下面代碼為例:
整個(gè)操作的內(nèi)存變化如下圖所示:

我們分析可以得出結(jié)論:
1.Python的字符串和列表底層都是胖指針的形式存儲(chǔ),列表指針的存儲(chǔ)內(nèi)容為:引用計(jì)數(shù),列表長(zhǎng)度,列表數(shù)據(jù)指針,列表容量。字符串指針的存儲(chǔ)內(nèi)容為:引用計(jì)數(shù),字符串長(zhǎng)度,文本數(shù)據(jù)內(nèi)容。
2.局部變量存儲(chǔ)在棧中。
3.賦值操作過(guò)程為,t = s,新建一個(gè)對(duì)象t,指向s的內(nèi)存地址,并將s對(duì)象的引用計(jì)數(shù)+1。u = s,再新建一個(gè)對(duì)象u,指向s的內(nèi)存地址,并將s對(duì)象的引用計(jì)數(shù)+1,s對(duì)象的引用計(jì)數(shù)為3,表示被3個(gè)對(duì)象使用。
4.釋放s內(nèi)存數(shù)據(jù)得維護(hù)s的引用計(jì)數(shù),引用計(jì)數(shù)為0時(shí)可以清理該內(nèi)存數(shù)據(jù)。
### C++
對(duì)應(yīng)的,C++賦值示例代碼如下:
該段代碼內(nèi)存結(jié)構(gòu)變化如下圖所示:

分析后,我們可以得出結(jié)論:
1.向量s局部變量在內(nèi)存中存儲(chǔ)在棧中。其也是一個(gè)胖指針,三個(gè)字段內(nèi)容為向量數(shù)據(jù)堆內(nèi)存地址,向量占用空間大小,向量長(zhǎng)度。堆內(nèi)存地址數(shù)據(jù)存儲(chǔ)也是三個(gè)胖指針,指針地址字段指向的分別是三個(gè)字符串的內(nèi)存地址。
2.t = s操作過(guò)程實(shí)際上是復(fù)制了一份s對(duì)象的數(shù)據(jù),包括堆內(nèi)存數(shù)據(jù),并將新的堆內(nèi)存數(shù)據(jù)指向t胖指針的堆內(nèi)存數(shù)據(jù)地址。
3.u = s操作過(guò)程和t = s操作過(guò)程一致。
4.完成賦值操作后,內(nèi)存中有三份s對(duì)象一樣的數(shù)據(jù),存儲(chǔ)在不同的堆中。
5.釋放s,t,u三個(gè)對(duì)象內(nèi)存很簡(jiǎn)單,各自維護(hù)自己生命周期即可。
### Rust
最后,我們?cè)賮?lái)看Rust賦值代碼:
Rust內(nèi)存結(jié)構(gòu)變化如下圖所示:

我們可以得出結(jié)論:
1.Rust中向量存儲(chǔ)和字符串存儲(chǔ)方式和C++一樣,都是胖指針,指針內(nèi)容格式一致。
2.t = s操作,將t胖指針的堆內(nèi)存數(shù)據(jù)地址指向s的堆內(nèi)存數(shù)據(jù)地址,s對(duì)象變成懸空指針,無(wú)法訪問(wèn)。
3.u = s操作,會(huì)報(bào)錯(cuò),此時(shí)s為懸空指針,不能訪問(wèn)。
4.向量s在堆內(nèi)存中數(shù)據(jù)只有一份。
5.釋放數(shù)據(jù)的操作也簡(jiǎn)單,因?yàn)槎褍?nèi)存中只有一份數(shù)據(jù),在脫離了作用域后會(huì)自動(dòng)釋放內(nèi)存數(shù)據(jù)。
### 三類(lèi)語(yǔ)言對(duì)比
對(duì)比C++,Python和Rust語(yǔ)言在相同賦值語(yǔ)句下,其內(nèi)存布局變化,我們可以很直觀的得出下面結(jié)論:
1.整個(gè)賦值過(guò)程占用內(nèi)存最小的是Rust語(yǔ)言和Python語(yǔ)言。
2.C++語(yǔ)言賦值操作最為笨重,需要做數(shù)據(jù)深度拷貝。
3.在釋放內(nèi)存操作時(shí),最高效簡(jiǎn)單的是Rust語(yǔ)言和C++語(yǔ)言。
4.Python語(yǔ)言在釋放內(nèi)存時(shí)需要維護(hù)引用計(jì)數(shù),較為復(fù)雜。
>注意,這里只是對(duì)比最常用情況,實(shí)際上Rust也支持類(lèi)似Python的引用計(jì)數(shù)內(nèi)存管理方法和C++的深度拷貝操作。有興趣可以去了解相關(guān)文檔。
## 引用介紹
在上述變量賦值操作過(guò)程中,實(shí)際上是一個(gè)變量所有權(quán)轉(zhuǎn)移的過(guò)程。那么,是否可以直接使用類(lèi)似C語(yǔ)言指針的方式去操作一個(gè)變量呢?答案是肯定的,Rust提供一種引用的數(shù)據(jù)類(lèi)型來(lái)完成此目的。
>Rust中對(duì)變量的引用,稱(chēng)之為借用(Borrowing),使用完畢后,需要?dú)w還。
我們來(lái)看一段示例代碼:
這段代碼中,是用了Rust的Rc包來(lái)創(chuàng)建一個(gè)可以被多個(gè)變量同時(shí)借用的引用變量,Rust中還提供一個(gè)Arc包來(lái)實(shí)現(xiàn)相同功能,區(qū)別是Rc非線程安全型,Arc是線程安全型的。Rc使用引用計(jì)數(shù)方式來(lái)實(shí)現(xiàn),Arc是Atomic Rc。Arc相比Rc會(huì)額外帶來(lái)性能損耗,需要用戶根據(jù)場(chǎng)景選用。
引用適用領(lǐng)域:
1.圖操作中一個(gè)點(diǎn)被多個(gè)邊包含。
2.一個(gè)變量被多個(gè)線程同時(shí)操作。
上段代碼在內(nèi)存中的存儲(chǔ)格式為:

為了保證整個(gè)機(jī)制在各個(gè)場(chǎng)景下的可靠,不出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)情況,Rust引入了所有權(quán)樹(shù)。
## 所有權(quán)樹(shù)
Rust中,所有權(quán)規(guī)則總結(jié)如下:
1.Rust中的每個(gè)值都有一個(gè)被稱(chēng)為其所有者的變量(即:值的所有者是某個(gè)變量)。
2.值在任一時(shí)刻有且只有一個(gè)所有者。
3.當(dāng)所有者(變量)離開(kāi)作用域,這個(gè)值將被銷(xiāo)毀。
如下圖所示,變量的所有權(quán)可以被借用,主要包括了可變引用和非可變引用。這里的可變引用和不可變引用可以使用讀寫(xiě)鎖來(lái)理解。通常來(lái)說(shuō),只讀的不可變引用即是只讀引用,變量可以被多個(gè)只讀引用來(lái)同時(shí)借用,由于是只讀的,所以不存在變量共享問(wèn)題。而可修改引用,則要求一個(gè)變量只能被一個(gè)可修改引用借用,而且,對(duì)該變量的訪問(wèn),只能通過(guò)該可變引用來(lái)訪問(wèn)。規(guī)則如下圖所示:

所有權(quán)樹(shù)表示了所有權(quán)行為都是可以推導(dǎo)的,也就是說(shuō)在編譯階段編譯器即可發(fā)現(xiàn)各類(lèi)的內(nèi)存管理問(wèn)題,所以這也是Rust內(nèi)存安全性的保證。
## 所有權(quán)示例代碼分析
>大家可以通過(guò)下面的代碼片段及后面注釋中的解釋來(lái)理解Rust的所有權(quán)樹(shù)。
1.代碼片段1:
2. 代碼片段2:
3. 代碼片段3:
## 思考
1.Rust中是否不存在內(nèi)存泄漏?
如下圖所示,在Rust中可以創(chuàng)建引用循環(huán),在此情況下,引用計(jì)數(shù)永遠(yuǎn)不可能為0,就會(huì)發(fā)生內(nèi)存泄漏。

為了避免該問(wèn)題,Rust中引入了RefCell機(jī)制,該機(jī)制不在本文詳細(xì)描述,大家可以搜索下相關(guān)文章,后續(xù)其他文章也會(huì)專(zhuān)門(mén)講解。