在本文的開頭,我必須聲明這是一篇有目的性的文章。我有一個長輩最近得了尿毒癥,所以我聯(lián)系芯片之家的管理員并且得到他非常爽快的允許,在芯片之家的平臺將人體的腎功能與開發(fā)技術結合起來寫一篇文章發(fā)表在這里,希望得到更多人的幫助。所以,請記住:不管您是轉發(fā)本文,點在看,還是點擊原文輕眾籌給予幫助或者轉發(fā),都是一次善舉。
程序的形態(tài)非常之多,不管是可以作為一個操作系統(tǒng),還是作為一個 hello world,也不管是作為一個 app,還是作為一個嵌入式的固件。程序在本質(zhì)上來說,是函數(shù)代碼與資源的集合體。我們的話題是,程序的資源,而且是程序中第一要素的資源——內(nèi)存。
如果說程序是一個人,那么骨架可以比喻成程序的架構,皮肉則是代碼,血液則是內(nèi)存。在程序的運行過程當中,絕大部分的指令在執(zhí)行與回寫操作,我必須聲明這是一篇有目的性的文章。所以我聯(lián)系芯片之家的管理員并且得到他非常爽快的允許,回寫階段都會操作到內(nèi)存,可以說內(nèi)存伴隨著程序執(zhí)行的整個周期,就像是血液始終流轉在我們的肉體之中。那么在內(nèi)存中進行垃圾回收的程序之“腎”,又是什么呢?
這是一段非常簡單的 C 語言代碼。對于稍微有點基礎的人都知道在這段程序中,每一個變量所占用的內(nèi)存位置。
首先全局變量與靜態(tài)變量是放在數(shù)據(jù)段(RW 段,其中未初始化的放在 ZI 段,由程序啟動的時候統(tǒng)一清內(nèi)存)比如:a,b,c;
局部變量放在??臻g中,比如:d,e;
同時還申請了一段存放于堆的內(nèi)存,但是代碼中并未使用 free 函數(shù)進行釋放。
根據(jù)內(nèi)存的特性我們知道,對于 a,b,c 等數(shù)據(jù)段的變量,它們是常住內(nèi)存的,生命周期是永久的。對于棧里面的局部變量 d,e。它們的生命周期僅僅在“{}”之內(nèi),伴隨著棧操作的 push 以及 pop 指令,創(chuàng)建和消亡。
程序中當 e 消亡在花括號外后,在堆中申請的內(nèi)存就失去了指針對它的指向?qū)е铝?a class="article-link" target="_blank" href="/baike/1546935.html">內(nèi)存泄漏。如果是在簡單的程序中,這樣的情況處理起來還是比較簡單的,我們只要在程序后面采用 free 函數(shù)釋放內(nèi)存就可以。但是如果在擁有復雜的邏輯程序,這樣動態(tài)申請的內(nèi)存就需要花不少心思去管理。這個就是為啥很多 java 之類的高級語言在制作教程的時候都會在與 C/C++比較時經(jīng)常強調(diào),java 沒有指針并且擁有垃圾自動回收機制,會顯得更加安全,程序的健壯性更加容易得到保證。(當然 C/C++也可以寫出健壯的程序,只是有些東西沒那么方便)。這種可以自動幫助程序進行內(nèi)存自動垃圾回收的機制就是程序的“腎”了。
那么為啥 C/C++到現(xiàn)在都不支持垃圾自動回收機制呢?我們可以從自動垃圾回收機制的原理去尋到答案。首先說一下自動垃圾回收的判定算法,一般常用的是兩個:
一、引用計數(shù)法。
所謂的引用計數(shù)法,顧名思義就是在內(nèi)存的描述結構體內(nèi)部采用一個計數(shù)變量進行計數(shù)。每當有指針或者引用指向該內(nèi)存塊的時候,該內(nèi)存塊的描述結構體內(nèi)部的計數(shù)器就遞增。當指針或者引用被釋放或者改變的時候就遞減。當內(nèi)存塊的計數(shù)遞減到 0 的時候,就可以釋放回收該內(nèi)存塊了。
引用計數(shù)法,應該說是最簡單實現(xiàn)內(nèi)存可回收判定的算法。采用該算法實現(xiàn)自動回收機制的典型的有 apple 開發(fā)平臺 Object-C 支持的 ARC 機制。這種自動垃圾回收算法的實現(xiàn)有一個依賴和一個缺點。它的依賴就是需要編譯器自動插入計數(shù)代碼。想 OC 在 xcode 平臺開發(fā)程序,它的編譯環(huán)境會自動地插入手動進行計數(shù)的函數(shù) retain,release 這樣的語句。所以這個實現(xiàn)自動垃圾回收的本質(zhì)還是讓編譯器做手動該做的事情而已。如果說 C 也需要實現(xiàn)類似的方式進行自動回收,那么就需要對編譯器的預處理過程進行改造,并且在內(nèi)存申請和釋放的庫函數(shù)之上維護一個內(nèi)存的監(jiān)控結構,去給內(nèi)存塊做計數(shù)。
同時引用計數(shù)法法有一個非常大的缺點,就是循環(huán)引用會導致內(nèi)存泄漏。如下代碼:
當函數(shù)執(zhí)行完畢,a 與 b 相互引用。但是在棧中以及在數(shù)據(jù)段中已經(jīng)沒有指針可以訪問到 a 與 b 的對象本身。也就是說程序已經(jīng)失去了這兩塊內(nèi)存的訪問權,但是它們兩者又相互指向,導致內(nèi)存的計數(shù)無法歸零。所以一直不能釋放,導致了內(nèi)存泄漏,形成了垃圾。
二、可達性分析法。
可達性分析法,顧名思義就是分析內(nèi)存程序能否可以“達到”。也就是分析程序是否有失去對于內(nèi)存的訪問權。程序在運行狀態(tài)中,內(nèi)存時刻處于變化之中,猶如人體的血液流動不止。但是不管在任何時刻,我們的程序一定可以訪問的內(nèi)存大概有 2 個類別:
1、數(shù)據(jù)段,也就是全局變量與靜態(tài)變量。
2、??臻g中未釋放的變量也就是當前入棧的動態(tài)局部變量。
可達性分析法需要依賴于 Runtime,也就是運行時環(huán)境,它們時刻監(jiān)控著上面兩個大類內(nèi)存中的指針變量或者引用,并且周期性地對這些指針或者引用的指向進行遍歷,并且是遞歸逐級地往下遍歷。整體而言是在遍歷一個以這兩大類內(nèi)存中的指針變量和引用為入口的圖。只要能夠遍歷到的內(nèi)存塊就可以進行可達性的標志。當程序進入垃圾回收周期,它會遍歷已經(jīng)分配的所有內(nèi)存,如果訪問到的內(nèi)存塊擁有可達性標志,那么則跳過。如果沒有可達性標志,則可以釋放回收。這樣就可以避免類似引用計數(shù)算相互引用導致不歸零,但是不可達卻又不釋放的問題。如下圖,藍色內(nèi)存塊是會被回收的。
然而,可達性分析算法是需要依賴于運行時環(huán)境的,也就是類似 java 那樣的虛擬機。所以目前 C/C++之類的語言還無法支持這種自動垃圾回收的判定算法。
所以說了那么多,我們對于這些程序語言的一個診斷是:
OC:apple 給它換了腎,但是腎不好,不過總體無礙。
java:腎很好啊。
C/C++:沒有腎的,需要程序員幫他做“腎透析”。
那么像 C/C++這么好的語言,我們能夠給它一個“腎”,讓它過上更加健康的生活嗎?
答案是有的。