官术网_书友最值得收藏!

6.變量和對象存儲在哪里?理解棧和堆

我們在編程過程中不斷地定義各種類型的變量,在面向?qū)ο蟮恼Z言中,我們還會經(jīng)常通過new關鍵字生成對象。通過第3節(jié)的學習,我們已經(jīng)理解了數(shù)據(jù)類型,但對于這些數(shù)據(jù)在內(nèi)存中是如何存儲的可能還存有疑問。變量和對象存儲在哪里?答案是棧和堆。經(jīng)常有人直接把內(nèi)存區(qū)分為棧內(nèi)存和堆內(nèi)存,這種方法比較粗糙,內(nèi)存區(qū)域的劃分實際比這復雜得多,但這種說法可以反映出與變量和對象的分配關系最為密切的內(nèi)存區(qū)域是這兩塊。通過學習本節(jié),讀者會對棧和堆形成深刻的理解,熟悉內(nèi)存模型是對一個程序員的基本要求,也是非常重要的一個要求。

進程地址空間

在學習棧和堆之前,讓我們先看一下Linux中的進程地址空間,從而對進程的內(nèi)存布局有一個全局的認識。圖6.1展示了Linux中的進程地址空間。

Linux操作系統(tǒng)的內(nèi)存分為兩大類,一類是內(nèi)核空間,一類是用戶空間,應用程序進程占用的內(nèi)存在用戶空間分配。一個Linux進程的地址空間分為圖6.1中顯示的幾個主要區(qū)域。

圖6.1 Linux中的進程地址空間

(1)棧:由操作系統(tǒng)自動分配和釋放,用于維護函數(shù)調(diào)用上下文,存儲函數(shù)的參數(shù)值、局部變量等。使用一級緩存,調(diào)用速度較快。

(2)堆:應用程序動態(tài)分配的內(nèi)存區(qū)域,一般由程序員分配和釋放(C/C++),若程序員不釋放,程序結(jié)束時由系統(tǒng)釋放(Java)。使用二級緩存,調(diào)用速度較慢。

(3)數(shù)據(jù)段:該內(nèi)存區(qū)域用于存放程序數(shù)據(jù),包括未初始化數(shù)據(jù)段(即均被初始化為0),初始化數(shù)據(jù)段。

(4)代碼段:該段數(shù)據(jù)存放程序代碼,具有執(zhí)行權限,只讀。

棧內(nèi)存

棧在數(shù)據(jù)結(jié)構中是一種具有先進后出特點的有序隊列,內(nèi)存中的棧的操作方式類似于數(shù)據(jù)結(jié)構中的棧。將棧的操作方式比作一堆碗碟,我們擁有兩種操作方式:可以在當前碗碟的頂部堆放一個新的碗碟,也可以將最頂上的碗碟取出,先堆進去的碗碟在最下面,最后才能取出。因此棧具有先進后出的特點,先入棧的元素后出棧。在碗碟的比喻中,這個棧的擴展方向是朝上的,而在Linux進程地址空間中,棧內(nèi)存的擴展方向是自頂向下的,如圖6.1所示。

想要理解棧內(nèi)存的工作原理,必須首先了解棧幀(Stack Frame),棧幀保存了一個函數(shù)調(diào)用的所有相關信息,每一個函數(shù)從調(diào)用到執(zhí)行完畢的過程,對應了一個棧幀在棧內(nèi)存中入棧到出棧的過程。一個棧幀主要包括以下幾部分內(nèi)容:

(1)函數(shù)參數(shù),該部分存儲函數(shù)的實參。

(2)函數(shù)返回地址,前一個棧幀的指針。該部分存儲恢復前一個棧幀所必需的數(shù)據(jù)。

(3)函數(shù)的局部變量。

(4)保存的上下文,即在函數(shù)調(diào)用前后需要保持不變的寄存器。

圖6.2 棧幀的結(jié)構

圖6.2展示了棧幀的結(jié)構,一個棧幀維護了一個函數(shù)調(diào)用的所有信息。一個棧幀維護了兩個指針,分別是ebp寄存器和esp寄存器。ebp是棧幀指針,該值指向了函數(shù)棧幀的一個固定位置,不隨函數(shù)的執(zhí)行變化。esp是棧幀棧頂指針,始終指向棧頂,會隨函數(shù)執(zhí)行不斷變化。因此,ebp可以用來唯一標識一個棧幀的位置。在圖6.2中可以看到有一個地址保存了舊的ebp的值,該值就是為了讓當前被調(diào)用函數(shù)執(zhí)行完畢后能夠找到調(diào)用函數(shù)的棧幀,從而找到調(diào)用函數(shù)的所有相關信息。從ebp正向偏移可以首先看到存放了函數(shù)的返回地址,函數(shù)的返回地址就是調(diào)用完該函數(shù)之后要執(zhí)行的下一條指令的地址。再向上可以看到存放了函數(shù)實參。從ebp負向偏移可以看到存放了函數(shù)調(diào)用前后需要保持不變的寄存器,以及函數(shù)的局部變量。

一個函數(shù)A的調(diào)用及其棧幀形成的過程如下:首先將函數(shù)A的參數(shù)依次(C語言中依照反向壓棧順序)入棧,接著將當前指令的下一條指令的地址(即函數(shù)A的返回地址)入棧,下面就開始執(zhí)行函數(shù)A,依次將函數(shù)A的局部變量入棧。當函數(shù)A執(zhí)行完畢,ebp恢復為舊的ebp的值,函數(shù)A的棧幀被銷毀,此時棧內(nèi)存棧頂為調(diào)用A的函數(shù)的棧幀,所以函數(shù)的參數(shù)和局部變量的作用域僅僅存在于函數(shù)內(nèi)部。本書的第7節(jié)有函數(shù)調(diào)用及其棧幀形成的具體示例,讀者可以通過閱讀第7節(jié)加深對棧內(nèi)存工作機制的理解。

堆內(nèi)存

由于棧幀的數(shù)據(jù)在函數(shù)返回的時候就被銷毀了,函數(shù)內(nèi)部的數(shù)據(jù)無法被傳遞到函數(shù)外部,僅僅用棧來存儲數(shù)據(jù)是不能滿足編程的需求的。因此,堆內(nèi)存應運而生。

如圖6.1所示,堆內(nèi)存的空間從低地址向高地址擴展,堆的存儲空間較棧要大得多。堆內(nèi)存的空間都是動態(tài)分配的,由于大量使用new和delete,堆內(nèi)存中更容易出現(xiàn)內(nèi)存碎片。

程序員可以隨時在堆內(nèi)存中申請空間。在C++中,程序員通過new或malloc動態(tài)申請堆內(nèi)存空間,而當程序員不再需要這片內(nèi)存空間時,需要通過delete或free主動釋放這片空間。由于程序員可能忘記釋放內(nèi)存這一操作,因此容易出現(xiàn)內(nèi)存泄漏的問題,關于內(nèi)存泄漏的定義讀者可以閱讀本書第10節(jié)。Java針對此問題作了改進,在Java中,程序員通過new申請堆內(nèi)存空間,而當這一內(nèi)存空間不再需要時,程序員無須主動釋放,Java虛擬機會對堆內(nèi)存中的對象實施垃圾回收機制,這些不再需要的內(nèi)存會由Java虛擬機自行回收并得到再次利用。本書第10節(jié)還詳細介紹了Java中的垃圾回收機制。

Java內(nèi)存分區(qū)

接下來我們將學習Java的內(nèi)存分區(qū),分析Java示例代碼中的各個變量和對象分別是如何存儲的。

JVM運行時數(shù)據(jù)區(qū)如圖6.3所示,JVM運行時會將它所管理的內(nèi)存劃分為若干不同區(qū)域,其中,Java堆內(nèi)存與方法區(qū)是由所有線程共享的數(shù)據(jù)區(qū),而虛擬機棧、本地方法棧、程序計數(shù)器是線程隔離的數(shù)據(jù)區(qū),各線程之間互不影響,各自獨立,這些區(qū)域是線程私有的內(nèi)存。

圖6.3 JVM運行時數(shù)據(jù)區(qū)

Java堆內(nèi)存是JVM管理的內(nèi)存中最大的區(qū)域,幾乎所有的對象實例(通過new生成的對象)和數(shù)組都在這里被分配內(nèi)存。Java堆內(nèi)存是垃圾收集器管理的主要區(qū)域,因此這一區(qū)域細分為“新生代”和“老生代”,其中新生代又被進一步劃分為Eden區(qū)、From Survivor區(qū)與To Survivor區(qū)。這樣劃分的目的是為了使JVM能夠更好地管理堆內(nèi)存中的對象,包括內(nèi)存的分配以及回收。

方法區(qū)用于存儲類信息、運行時常量、靜態(tài)變量等。很多程序員將這一區(qū)域稱為“永久代”,嚴格說這兩者并不等價。這一區(qū)域的垃圾回收較少出現(xiàn),但并非所有數(shù)據(jù)進入方法區(qū)就不會被回收了。運行時常量池是方法區(qū)的一部分,該區(qū)域用于存放編譯期生成的各種字面量和符號引用。

程序計數(shù)器是一片較小的內(nèi)存空間,該區(qū)域記錄正在執(zhí)行的虛擬機字節(jié)碼的地址。JVM在切換線程時為了恢復到對應線程的執(zhí)行位置,需要查看程序計數(shù)器。

Java虛擬機棧就是Java中的棧內(nèi)存,該區(qū)域是線程私有的,生命周期與線程相同。Java虛擬機棧主要存儲了函數(shù)的局部變量,包括各種基本數(shù)據(jù)類型和引用類型(不同于對象本身,對象本身存儲在堆內(nèi)存中),這些局部變量存在于函數(shù)對應的棧幀中,作用域為函數(shù)。

本地方法棧類似于Java虛擬機棧,區(qū)別在于,虛擬機棧執(zhí)行的是Java方法,而本地方法棧執(zhí)行的是Native方法服務。

變量和對象存儲在哪里?

示例代碼6.1

為了更好地理解本節(jié)一開始提出的問題“變量和對象存儲在哪里”,我們不妨結(jié)合示例代碼6.1來最后總結(jié)一下這個問題的答案。

在示例代碼6.1的第4行,我們首先定義了一個int類型的局部變量num,根據(jù)本節(jié)的內(nèi)容可知,該局部變量存儲在main函數(shù)對應的棧幀中。

在示例代碼6.1的第5行,我們先來看等號右邊的內(nèi)容,這里通過new Object()我們生成了一個Object類型的對象,該動態(tài)生成的對象存儲在Java堆內(nèi)存中。但第5行代碼不止生成了對象,我們再來看等號左邊的內(nèi)容,這里定義了一個Object類型引用ref,值得注意的是,ref本身不是對象,而是一個引用。Thinking in Java一書將引用比作遙控器,將對象比作電視機,程序員所有對于電視機的操作都是通過遙控器實現(xiàn)的。在這一行代碼中,ref引用實際上存儲著等號右邊生成的Object類型對象在堆內(nèi)存中的地址,有了這個對象的地址,我們就能夠操縱該對象了。引用類型不同于對象,引用存儲在棧內(nèi)存中,示例代碼中ref引用存儲在main函數(shù)對應的棧幀中。引用和對象的關系如圖6.4所示。關于Java中引用與對象的更多知識,讀者可以閱讀本書第9節(jié)。

圖6.4 引用和對象的存儲關系

主站蜘蛛池模板: 额尔古纳市| 秦安县| 沙坪坝区| 武功县| 四川省| 阳谷县| 泾阳县| 新邵县| 汉阴县| 新乐市| 古浪县| 抚宁县| 大足县| 沙河市| 五常市| 芷江| 锡林浩特市| 永胜县| 布拖县| 灌云县| 芜湖市| 安庆市| 久治县| 蕉岭县| 陈巴尔虎旗| 吐鲁番市| 铜梁县| 右玉县| 宁晋县| 兴仁县| 桦川县| 旺苍县| 崇礼县| 汉阴县| 武宣县| 宜阳县| 封丘县| 城口县| 七台河市| 绥化市| 永顺县|