程序也有“腎”,你知道是什么嗎?
ID:chiphome-dy
作者:程世輝
整理排版:曉宇
int* a;int b =2;main(){static int c;{int d = 9;char* e = malloc(10);printf("d=%d\r\n",d);}}
這是一段非常簡單的C語言代碼。對于稍微有點(diǎn)基礎(chǔ)的人都知道在這段程序中,每一個變量所占用的內(nèi)存位置。
首先全局變量與靜態(tài)變量是放在數(shù)據(jù)段(RW段,其中未初始化的放在ZI段,由程序啟動的時候統(tǒng)一清內(nèi)存)比如:a,b,c;
局部變量放在??臻g中,比如:d,e;
同時還申請了一段存放于堆的內(nèi)存,但是代碼中并未使用free函數(shù)進(jìn)行釋放。
根據(jù)內(nèi)存的特性我們知道,對于a,b,c等數(shù)據(jù)段的變量,它們是常住內(nèi)存的,生命周期是永久的。對于棧里面的局部變量d,e。它們的生命周期僅僅在“{}”之內(nèi),伴隨著棧操作的push以及pop指令,創(chuàng)建和消亡。
程序中當(dāng)e消亡在花括號外后,在堆中申請的內(nèi)存就失去了指針對它的指向?qū)е铝藘?nèi)存泄漏。如果是在簡單的程序中,這樣的情況處理起來還是比較簡單的,我們只要在程序后面采用free函數(shù)釋放內(nèi)存就可以。
但是如果在擁有復(fù)雜的邏輯程序,這樣動態(tài)申請的內(nèi)存就需要花不少心思去管理。這個就是為啥很多java之類的高級語言在制作教程的時候都會在與C/C++比較時經(jīng)常強(qiáng)調(diào),java沒有指針并且擁有垃圾自動回收機(jī)制,會顯得更加安全,程序的健壯性更加容易得到保證。(當(dāng)然C/C++也可以寫出健壯的程序,只是有些東西沒那么方便)。這種可以自動幫助程序進(jìn)行內(nèi)存自動垃圾回收的機(jī)制就是程序的“腎”了。
那么為啥C/C++到現(xiàn)在都不支持垃圾自動回收機(jī)制呢?我們可以從自動垃圾回收機(jī)制的原理去尋到答案。首先說一下自動垃圾回收的判定算法,一般常用的是兩個:
一、引用計數(shù)法。
所謂的引用計數(shù)法,顧名思義就是在內(nèi)存的描述結(jié)構(gòu)體內(nèi)部采用一個計數(shù)變量進(jìn)行計數(shù)。每當(dāng)有指針或者引用指向該內(nèi)存塊的時候,該內(nèi)存塊的描述結(jié)構(gòu)體內(nèi)部的計數(shù)器就遞增。當(dāng)指針或者引用被釋放或者改變的時候就遞減。當(dāng)內(nèi)存塊的計數(shù)遞減到0的時候,就可以釋放回收該內(nèi)存塊了。
引用計數(shù)法,應(yīng)該說是最簡單實(shí)現(xiàn)內(nèi)存可回收判定的算法。采用該算法實(shí)現(xiàn)自動回收機(jī)制的典型的有apple開發(fā)平臺Object-C支持的ARC機(jī)制。這種自動垃圾回收算法的實(shí)現(xiàn)有一個依賴和一個缺點(diǎn)。它的依賴就是需要編譯器自動插入計數(shù)代碼。
想OC在xcode平臺開發(fā)程序,它的編譯環(huán)境會自動地插入手動進(jìn)行計數(shù)的函數(shù)retain,release這樣的語句。所以這個實(shí)現(xiàn)自動垃圾回收的本質(zhì)還是讓編譯器做手動該做的事情而已。
如果說C也需要實(shí)現(xiàn)類似的方式進(jìn)行自動回收,那么就需要對編譯器的預(yù)處理過程進(jìn)行改造,并且在內(nèi)存申請和釋放的庫函數(shù)之上維護(hù)一個內(nèi)存的監(jiān)控結(jié)構(gòu),去給內(nèi)存塊做計數(shù)。
同時引用計數(shù)法法有一個非常大的缺點(diǎn),就是循環(huán)引用會導(dǎo)致內(nèi)存泄漏。如下代碼:
fun(){A* a = [ A new];A* a1 = a;B* b = [B new];B* b1 = b;a->b = b;b ->a = a;}
當(dāng)函數(shù)執(zhí)行完畢,a與b相互引用。但是在棧中以及在數(shù)據(jù)段中已經(jīng)沒有指針可以訪問到a與b的對象本身。也就是說程序已經(jīng)失去了這兩塊內(nèi)存的訪問權(quán),但是它們兩者又相互指向,導(dǎo)致內(nèi)存的計數(shù)無法歸零。所以一直不能釋放,導(dǎo)致了內(nèi)存泄漏,形成了垃圾。
二、可達(dá)性分析法。
可達(dá)性分析法,顧名思義就是分析內(nèi)存程序能否可以“達(dá)到”。也就是分析程序是否有失去對于內(nèi)存的訪問權(quán)。程序在運(yùn)行狀態(tài)中,內(nèi)存時刻處于變化之中,猶如人體的血液流動不止。但是不管在任何時刻,我們的程序一定可以訪問的內(nèi)存大概有2個類別:
1、數(shù)據(jù)段,也就是全局變量與靜態(tài)變量。
2、??臻g中未釋放的變量也就是當(dāng)前入棧的動態(tài)局部變量。
可達(dá)性分析法需要依賴于Runtime,也就是運(yùn)行時環(huán)境,它們時刻監(jiān)控著上面兩個大類內(nèi)存中的指針變量或者引用,并且周期性地對這些指針或者引用的指向進(jìn)行遍歷,并且是遞歸逐級地往下遍歷。整體而言是在遍歷一個以這兩大類內(nèi)存中的指針變量和引用為入口的圖。只要能夠遍歷到的內(nèi)存塊就可以進(jìn)行可達(dá)性的標(biāo)志。
當(dāng)程序進(jìn)入垃圾回收周期,它會遍歷已經(jīng)分配的所有內(nèi)存,如果訪問到的內(nèi)存塊擁有可達(dá)性標(biāo)志,那么則跳過。如果沒有可達(dá)性標(biāo)志,則可以釋放回收。這樣就可以避免類似引用計數(shù)算相互引用導(dǎo)致不歸零,但是不可達(dá)卻又不釋放的問題。如下圖,藍(lán)色內(nèi)存塊是會被回收的。

然而,可達(dá)性分析算法是需要依賴于運(yùn)行時環(huán)境的,也就是類似java那樣的虛擬機(jī)。所以目前C/C++之類的語言還無法支持這種自動垃圾回收的判定算法。
所以說了那么多,我們對于這些程序語言的一個診斷是:
OC:apple給它換了腎,但是腎不好,不過總體無礙。
java:腎很好啊。
C/C++:沒有腎的,需要程序員幫他做“腎透析”。
那么像C/C++這么好的語言,我們能夠給它一個“腎”,讓它過上更加健康的生活嗎?
答案是有的。
推薦閱讀:嵌入式編程專輯 Linux 學(xué)習(xí)專輯 C/C++編程專輯 Qt進(jìn)階學(xué)習(xí)專輯 關(guān)注微信公眾號『技術(shù)讓夢想更偉大』,后臺回復(fù)“m”查看更多內(nèi)容,回復(fù)“加群”加入技術(shù)交流群。 長按前往圖中包含的公眾號關(guān)注
