學過c語言的朋友都知道c編譯器在劃分記憶體區域的時候經常将管理的區域劃分為資料段和代碼段,資料段包括堆、棧以及靜态資料區。那麼在java語言當中,記憶體又是如何劃分的呢?
由于java程式是交由jvm執行的,是以我們在談java記憶體區域劃分的時候事實上是指jvm記憶體區域劃分。在讨論jvm記憶體區域劃分之前,先來看一下java程式具體執行的過程:

如上圖所示,首先java源代碼檔案(.java字尾)會被java編譯器編譯為位元組碼檔案(.class字尾),然後由jvm中的類加載器加載各個類的位元組碼檔案,加載完畢之後,交由jvm執行引擎執行。在整個程式執行過程中,jvm會用一段空間來存儲程式執行期間需要用到的資料和相關資訊,這段空間一般被稱作為runtime data area(運作時資料區),也就是我們常說的jvm記憶體。是以,在java中我們常常說到的記憶體管理就是針對這段空間進行管理(如何配置設定和回收記憶體空間)。
在知道了jvm記憶體是什麼東西之後,下面我們就來讨論一下這段空間具體是如何劃分區域的,是不是也像c語言中一樣也存在棧和堆呢?
根據《java虛拟機規範》的規定,運作時資料區通常包括這幾個部分:程式計數器(program counter register)、java棧(vm stack)、本地方法棧(native method stack)、方法區(method area)、堆(heap)。
如上圖所示,jvm中的運作時資料區應該包括這些部分。在jvm規範中雖然規定了程式在執行期間運作時資料區應該包括這幾部分,但是至于具體如何實作并沒有做出規定,不同的虛拟機廠商可以有不同的實作方式。
下面我們來了解一下運作時資料區的每部分具體用來存儲程式執行過程中的哪些資料。
程式計數器(program counter register),也有稱作為pc寄存器。想必學過彙編語言的朋友對程式計數器這個概念并不陌生,在彙編語言中,程式計數器是指cpu中的寄存器,它儲存的是程式目前執行的指令的位址(也可以說儲存下一條指令的所在存儲單元的位址),當cpu需要執行指令時,需要從程式計數器中得到目前需要執行的指令所在存儲單元的位址,然後根據得到的位址擷取到指令,在得到指令之後,程式計數器便自動加1或者根據轉移指針得到下一條指令的位址,如此循環,直至執行完所有的指令。
雖然jvm中的程式計數器并不像彙編語言中的程式計數器一樣是實體概念上的cpu寄存器,但是jvm中的程式計數器的功能跟彙編語言中的程式計數器的功能在邏輯上是等同的,也就是說是用來訓示 執行哪條指令的。
由于在jvm中,多線程是通過線程輪流切換來獲得cpu執行時間的,是以,在任一具體時刻,一個cpu的核心隻會執行一條線程中的指令,是以,為了能夠使得每個線程都線上程切換後能夠恢複在切換之前的程式執行位置,每個線程都需要有自己獨立的程式計數器,并且不能互相被幹擾,否則就會影響到程式的正常執行次序。是以,可以這麼說,程式計數器是每個線程所私有的。
在jvm規範中規定,如果線程執行的是非native方法,則程式計數器中儲存的是目前需要執行的指令的位址;如果線程執行的是native方法,則程式計數器中的值是undefined。
由于程式計數器中存儲的資料所占空間的大小不會随程式的執行而發生改變,是以,對于程式計數器是不會發生記憶體溢出現象(outofmemory)的。
java棧也稱作虛拟機棧(java vitual machine stack),也就是我們常常所說的棧,跟c語言的資料段中的棧類似。事實上,java棧是java方法執行的記憶體模型。為什麼這麼說呢?下面就來解釋一下其中的原因。
java棧中存放的是一個個的棧幀,每個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(local variables)、操作數棧(operand
stack)、指向目前方法所屬的類的運作時常量池(運作時常量池的概念在方法區部分會談到)的引用(reference to runtime constant pool)、方法傳回位址(return address)和一些額外的附加資訊。當線程執行一個方法時,就會随之建立一個對應的棧幀,并将建立的棧幀壓棧。當方法執行完畢之後,便會将棧幀出棧。是以可知,線程目前執行的方法所對應的棧幀必定位于java棧的頂部。講到這裡,大家就應該會明白為什麼 在 使用 遞歸方法的時候容易導緻棧記憶體溢出的現象了以及為什麼棧區的空間不用程式員去管理了(當然在java中,程式員基本不用關系到記憶體配置設定和釋放的事情,因為java有自己的垃圾回收機制),這部分空間的配置設定和釋放都是由系統自動實施的。對于所有的程式設計語言來說,棧這部分空間對程式員來說是不透明的。下圖表示了一個java棧的模型:
局部變量表,顧名思義,想必不用解釋大家應該明白它的作用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜态變量以及函數形參)。對于基本資料類型的變量,則直接存儲它的值,對于引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以确定其大小了,是以在程式執行期間局部變量表的大小是不會改變的。
操作數棧,想必學過資料結構中的棧的朋友想必對表達式求值問題不會陌生,棧最典型的一個應用就是用來對表達式求值。想想一個線程執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。是以可以這麼說,程式中的所有計算過程都是在借助于操作數棧來完成的。
指向運作時常量池的引用,因為在方法執行的過程中有可能需要用到類中的常量,是以必須要有一個引用指向運作時常量。
方法傳回位址,當一個方法執行完畢之後,要傳回之前調用它的地方,是以在棧幀中必須儲存一個方法傳回位址。
由于每個線程正在執行的方法可能不同,是以每個線程都會有一個自己的java棧,互不幹擾。
本地方法棧與java棧的作用和原理非常相似。差別隻不過是java棧是為執行java方法服務的,而本地方法棧則是為執行本地方法(native method)服務的。在jvm規範中,并沒有對本地方發展的具體實作方法以及資料結構作強制規定,虛拟機可以自由實作它。在hotsopt虛拟機中直接就把本地方法棧和java棧合二為一。
在c語言中,堆這部分空間是唯一一個程式員可以管理的記憶體區域。程式員可以通過malloc函數和free函數在堆上申請和釋放空間。那麼在java中是怎麼樣的呢?
java中的堆是用來存儲對象本身的以及數組(當然,數組引用是存放在java棧中的)。隻不過和c語言中的不同,在java中,程式員基本不用去關心空間釋放的問題,java的垃圾回收機制會自動進行處理。是以這部分空間也是java垃圾收集器管理的主要區域。另外,堆是被所有線程共享的,在jvm中隻有一個堆。
方法區在jvm中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的資訊(包括類的名稱、方法資訊、字段資訊)、靜态變量、常量以及編譯器編譯後的代碼等。
在class檔案中除了類的字段、方法、接口等描述資訊外,還有一項資訊是常量池,用來存儲編譯期間生成的字面量和符号引用。
在方法區中有一個非常重要的部分就是運作時常量池,它是每一個類或接口的常量池的運作時表示形式,在類和接口被加載到jvm後,對應的運作時常量池就被建立出來。當然并非class檔案常量池中的内容才能進入運作時常量池,在運作期間也可将新的常量放入運作時常量池中,比如string的intern方法。
在jvm規範中,沒有強制要求方法區必須實作垃圾回收。很多人習慣将方法區稱為“永久代”,是因為hotspot虛拟機以永久代來實作方法區,進而jvm的垃圾收集器可以像管理堆區一樣管理這部分區域,進而不需要專門為這部分設計垃圾回收機制。不過自從jdk7之後,hotspot虛拟機便将運作時常量池從永久代移除了。
以上為個人看法和觀點,如有不正之處希望諒解并歡迎指正。
參考資料:
《深入了解java虛拟機》
《java虛拟機規範 se7》