1. 程式人生 > >(一)執行緒------JVM體系結構與記憶體模型概要

(一)執行緒------JVM體系結構與記憶體模型概要

    為了徹底搞清楚執行緒問題,特寫此係列文章記錄之。這一些列文章將講述JVM的體系結構以及涉及執行緒相關的JVM的執行時資料區來引出Java記憶體模型,Java通訊原理將描述執行緒間資料通訊存在的問題以及執行緒同步的重要性。Java執行緒狀態講述執行緒的基本知識。Java執行緒鎖機制講述執行緒同步機制的解決辦法。

JVM體系結構

                 
    類載入器:     每一個被JVM裝載的型別都有一個與之對應的Java.lang.Class類的例項來表示該型別。該例項可以唯一表示被jvm裝載的class類,這個例項和其他類的例項一樣放在堆記憶體中。     執行引擎 :
    執行引擎相當於執行緒,是JVM的核心,執行引擎的作用就是解析JVM位元組碼指令,得到執行的結果。執行引擎由各個廠家實現。SUN的hotspot是一種基於棧的執行引擎。而Android的Dalvik是基於暫存器的執行引擎。執行引擎也就是執行一條條程式碼的一個流程,程式碼都包含在方法體中,執行引擎本質上就是執行一個個方法串起來的流程,對應於作業系統的一個執行緒,每個java執行緒就是一個執行引擎的例項。     Java虛擬機器的主要任務就是裝在class檔案並執行相應的位元組碼檔案。從JVM體系結構圖中我們看出:一個JVM例項由類裝載器、執行時資料區以及執行引擎所構成。一個Java程式的執行流程為:首先Java原始碼檔案(.java字尾)會被Java編譯器編譯為位元組碼檔案(.class字尾),然後由JVM中的類載入器載入各個類的位元組碼檔案,載入完畢之後,交由JVM執行引擎執行。那麼本系列不會講解類載入器,主要圍繞執行時資料區講解執行緒相關問題。所以接下來我們就來了解一下執行時資料區(就是JVM記憶體)。
Java記憶體模型     Java程式碼是執行在Java虛擬機器之上的,由Java虛擬機器通過解釋執行(直譯器)或編譯執行(即時編譯器)來完成,故Java記憶體模型,也就是指Java虛擬機器的執行時記憶體模型。 OK,我們通過一張圖進一步了:                       

    程式計數器:

        程式計數器(Program Counter Register),也有稱作為PC暫存器。想必學過組合語言的朋友對程式計數器這個概念並不陌生,在組合語言中,程式計數器是指CPU中的暫存器,它儲存的是程式當前執行的指令的地址(也可以說儲存下一條指令的所在儲存單元的地址),當CPU需要執行指令時,需要從程式計數器中得到當前需要執行的指令所在儲存單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程式計數器便自動加1或者根據轉移指標得到下一條指令的地址,如此迴圈,直至執行完所有的指令。
  雖然JVM中的程式計數器並不像組合語言中的程式計數器一樣是物理概念上的CPU暫存器,但是JVM中的程式計數器的功能跟組合語言中的程式計數器的功能在邏輯上是等同的,也就是說是用來指示執行哪條指令的。
  由於在JVM中,多執行緒是通過執行緒輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的核心只會執行一條執行緒中的指令,因此,為了能夠使得每個執行緒都線上程切換後能夠恢復在切換之前的程式執行位置,每個執行緒都需要有自己獨立的程式計數器,並且不能互相被幹擾,否則就會影響到程式的正常執行次序。因此,可以這麼說,程式計數器是每個執行緒所私有的

  在JVM規範中規定,如果執行緒執行的是非native方法,則程式計數器中儲存的是當前需要執行的指令的地址;如果執行緒執行的是native方法,則程式計數器中的值是undefined。
  由於程式計數器中儲存的資料所佔空間的大小不會隨程式的執行而發生改變,因此,對於程式計數器是不會發生記憶體溢位現象(OutOfMemory)的。

   虛擬機器棧

  虛擬機器棧(Java Vitual Machine Stack),也就是我們常常所說的棧,跟C語言的資料段中的棧類似。事實上,Java棧是Java方法執行的記憶體模型。為什麼這麼說呢?下面就來解釋一下其中的原因。
  Java棧中存放的是一個個的棧幀,每個棧幀對應一個被呼叫的方法,在棧幀中包括區域性變量表(Local Variables)、運算元棧(Operand Stack)、指向當前方法所屬的類的執行時常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加資訊。當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。因此可知,執行緒當前執行的方法所對應的棧幀必定位於Java棧的頂部。講到這裡,大家就應該會明白為什麼在使用遞迴方法的時候容易導致棧記憶體溢位(StackOverFlowError)的現象了以及為什麼棧區的空間不用程式設計師去管理了(當然在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虛擬機器便將執行時常量池從永久代移除了。    結論:      (1)執行緒私有區,包含以下3類:              程式計數器,記錄正在執行的虛擬機器位元組碼的地址;              虛擬機器棧:方法執行的記憶體區,每個方法執行時會在虛擬機器棧中建立棧幀;              本地方法棧:虛擬機器的Native方法執行的記憶體區;     (2)執行緒共享區,包含以下2類             Java堆:物件分配記憶體的區域;             方法區:存放類資訊、常量、靜態變數、編譯器編譯後的程式碼等資料;             常量池:存放編譯器生成的各種字面量和符號引用,是方法區的一部分。     通過上述的描述,我們把記憶體模型圖進一步細分如下: