天天看點

(一)運作時資料區域之程式計數器、虛拟機棧、本地方法棧、堆、方法區

(一)運作時資料區域之程式計數器、虛拟機棧、本地方法棧、堆、方法區

一、程式計數器

程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是目前線程所執行的位元組碼的行号訓示器。 

    通俗地講,線程執行的任務在計算機語言中,被當做是一條條的指令。線程需要一個計數器來幫助它标記執行了什麼指令,以及選取下一條指令。位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。

  每條線程被CPU執行之後(因為java虛拟機的多線程是通過線程輪流切換并配置設定處理器執行時間來實作的--時間片輪轉),需要切換下一條,為了使線程能恢複到正确的執行位置,每條線程都需要有一個獨立的程式計數器,各線程之間的計數器互不影響,獨立存儲。

  這一塊記憶體區域為“線程私有”的記憶體。

二、Java 虛拟機棧(具體應該叫做方法調用棧)

     Java 虛拟機棧(Java Virtual Machine Stacks)也是線程私有的,生命周期與線程相同,因為它描述的是Java方法執行的記憶體模型。

      Java棧是一個線程的執行區域, 它儲存着一個線程中的方法的調用狀态, 也可以說, 一個Java線程的運作狀态, 都由一個Java棧來儲存。 在這個棧中, 每一方法對應一個棧幀, 請注意區分棧幀和棧這兩個概念。 棧指的是整個線程的執行棧, 棧幀是棧中的一個機關, 每個方法對應一個棧幀(棧幀随着一個方法的調用開始而建立,這個方法調用完成而銷毀)。 JVM會對Java棧執行兩種操作: 壓棧和出棧。 這兩種操作在執行時都是以幀(棧幀)為機關的。 當調用了一個新的方法, 就會壓入一個棧幀, 當一個方法調用完成, 就會彈出這個方法的棧幀, 回到調用者的棧幀。 

     舉例來說, 如果方法a調用了方法b, 而方法b中調用了方法c。 這個過程中的方法調用和傳回的裝狀态是這樣的(其中圖中兩條虛線之間表示Java棧,每個方塊表示一個特定方法的棧幀)

(一)運作時資料區域之程式計數器、虛拟機棧、本地方法棧、堆、方法區

一個線程中方法的調用鍊可能會很長,很多方法都同時處于執行狀态。對于JVM執行引擎來說,在在活動線程中,隻有位于JVM虛拟機棧棧頂的元素才是有效的,即稱為目前棧幀,與這個棧幀相關連的方法稱為目前方法,定義這個方法的類叫做目前類。 

(一)運作時資料區域之程式計數器、虛拟機棧、本地方法棧、堆、方法區

由上圖可以看出,Java棧中存放的是一個個的棧幀,每個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操作數棧(Operand Stack)、指向目前方法所屬的類的運作時常量池(運作時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法傳回位址(Return Address)和一些額外的附加資訊。當線程執行一個方法時,就會随之建立一個對應的棧幀,并将建立的棧幀壓棧。當方法執行完畢之後,便會将棧幀出棧。是以可知,線程目前執行的方法所對應的棧幀必定位于Java棧的頂部。對于所有的程式設計語言來說,棧這部分空間對程式員來說是不透明的。 

  每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中入棧到出棧的過程。

  在Java虛拟機的規範中,對這個區域規定了兩種異常情況:

①局部變量表(Local Variable Table) 

StackOverflowError

  如果線程請求的棧深度大于虛拟機所允許的深度,将抛出 StackOverflowError異常

OutOfMemoryError

  如果虛拟機棧在擴充的時無法申請到足夠的記憶體,就會抛出 OutOfMemoryError異常

局部變量表(Local Variable Table)是一組變量值存儲空間,用于存放方法參數和方法内定義的局部變量。局部變量表的容量以變量槽(Variable Slot)為最小機關,Java虛拟機規範并沒有定義一個槽所應該占用記憶體空間的大小,但是規定了一個槽應該可以存放一個32位以内的資料類型。

在Java程式編譯為Class檔案時,就在方法的Code屬性中的max_locals資料項中确定了該方法所需配置設定的局部變量表的最大容量。(最大Slot數量)

一個局部變量可以儲存一個類型為boolean、byte、char、short、int、float、reference和returnAddress類型的資料。reference類型表示對一個對象執行個體的引用。returnAddress類型是為jsr、jsr_w和ret指令服務的,目前已經很少使用了。

虛拟機通過索引定位的方法查找相應的局部變量,索引的範圍是從0~局部變量表最大容量。如果Slot是32位的,則遇到一個64位資料類型的變量(如long或double型),則會連續使用兩個連續的Slot來存儲。

②操作數

操作數就是操作資料

     Java 程式編譯之後就變成了一條條位元組碼指令,其形式類似彙編,但和彙編有不同之處:彙編指令的操作數存放在資料段和寄存器中,可通過存儲器或寄存器尋址找到需要的操作數;而 Java 位元組碼指令的操作數存放在操作數棧中,當執行某條帶 n 個操作數的指令時,就從棧頂取 n 個操作數,然後把指令的計算結果(如果有的話)入棧。是以,當我們說 JVM 執行引擎是基于棧的時候,其中的“棧”指的就是操作數棧。舉個簡單的例子對比下彙編指令和 Java 位元組碼指令的執行過程,比如計算 1 + 2,在彙編指令是這樣的:

1

2

<code>mov ax,</code><code>1</code> <code>;把</code><code>1</code> <code>放入寄存器 ax</code>

<code>add ax,</code><code>2</code> <code>;用 ax 的内容和</code><code>2</code> <code>相加後存入 ax</code>

而 JVM 的位元組碼指令是這樣的:

3

<code>iconst_1</code><code>//把整數 1 壓入操作數棧</code>

<code>iconst_2</code><code>//把整數 2 壓入操作數棧</code>

<code>iadd</code><code>//棧頂的兩個數相加後出棧,結果入棧</code>

由于操作數棧是記憶體空間,是以位元組碼指令不必擔心不同機器上寄存器以及機器指令的差别,進而做到了平台無關。

注意,局部變量表中的變量不可直接使用,如需使用必須通過相關指令将其加載至操作數棧中作為操作數使用。比如有一個方法 void foo(),其中的代碼為:int a = 1 + 2; int b = a + 3;,編譯為位元組碼指令就是這樣的:

4

5

6

7

8

9

<code>iadd</code><code>//棧頂的兩個數出棧後相加,結果入棧;實際上前三步會被編譯器優化為:iconst_3</code>

<code>istore_1</code><code>//把棧頂的内容放入局部變量表中索引為 1 的 slot 中,也就是 a 對應的空間中</code>

<code>iload_1</code><code>// 把局部變量表索引為 1 的 slot 中存放的變量值(3)加載至操作數棧</code>

<code>iconst_3</code>

<code>iadd</code><code>//棧頂的兩個數出棧後相加,結果入棧</code>

<code>istore_2</code><code>// 把棧頂的内容放入局部變量表中索引為 2 的 slot 中,也就是 b 對應的空間中</code>

<code>return</code> <code>// 方法傳回指令,回到調用點</code>

需要說明的是,局部變量表以及操作數棧的容量的最大值在編譯時就已經确定了,運作時不會改變。并且局部變量表的空間是可以複用的,例如,當指令的位置超出了局部變量表中某個變量 a 的作用域時,如果有新的局部變量 b 要被定義,b 就會覆寫 a 在局部變量表的空間。

③動态連接配接

在一個class檔案中,一個方法要調用其他方法,需要将這些方法的符号引用轉化為其在記憶體位址中的直接引用,而符号引用存在于方法區中的運作時常量池。

Java虛拟機棧中,每個棧幀都包含一個指向運作時常量池中該棧所屬方法的符号引用,持有這個引用的目的是為了支援方法調用過程中的動态連接配接(Dynamic Linking)。

這些符号引用一部分會在類加載階段或者第一次使用時就直接轉化為直接引用,這類轉化稱為靜态解析。另一部分将在每次運作期間轉化為直接引用,這類轉化稱為動态連接配接。

④方法傳回

當一個方法開始執行時,可能有兩種方式退出該方法:

正常完成出口

異常完成出口

正常完成出口是指方法正常完成并退出,沒有抛出任何異常(包括Java虛拟機異常以及執行時通過throw語句顯示抛出的異常)。如果目前方法正常完成,則根據目前方法傳回的位元組碼指令,這時有可能會有傳回值傳遞給方法調用者(調用它的方法),或者無傳回值。具體是否有傳回值以及傳回值的資料類型将根據該方法傳回的位元組碼指令确定。

異常完成出口是指方法執行過程中遇到異常,并且這個異常在方法體内部沒有得到處理,導緻方法退出。

無論是Java虛拟機抛出的異常還是代碼中使用athrow指令産生的異常,隻要在本方法的異常表中沒有搜尋到相應的異常處理器,就會導緻方法退出。

無論方法采用何種方式退出,在方法退出後都需要傳回到方法被調用的位置,程式才能繼續執行,方法傳回時可能需要在目前棧幀中儲存一些資訊,用來幫他恢複它的上層方法執行狀态。

方法退出過程實際上就等同于把目前棧幀出棧,是以退出可以執行的操作有:恢複上層方法的局部變量表和操作數棧,把傳回值(如果有的話)壓如調用者的操作數棧中,調整PC計數器的值以指向方法調用指令後的下一條指令。

一般來說,方法正常退出時,調用者的PC計數值可以作為傳回位址,棧幀中可能儲存此計數值。而方法異常退出時,傳回位址是通過異常處理器表确定的,棧幀中一般不會儲存此部分資訊。

總結 

1. 每個線程包含一個棧區,棧中局部變量表儲存基礎資料類型的對象和自定義對象的引用(不是對象)。對象都存放在堆區中。 

2. 每個棧中的資料(基礎資料類型和對象引用)都是私有的,其他棧不能通路。 

3. 棧分為3個部分:基本類型變量,執行環境上下文,操作指令區(存放操作指令). 

4. 在函數中定義的一些基本類型的變量資料和對象的引用變量都在函數的棧記憶體中配置設定。 

5. 當在一段代碼塊定義一個變量時,Java就在棧中為這個變量配置設定記憶體空間,當該變量退出該作用域後,Java會自動釋放掉為該變量所配置設定的記憶體空間,該記憶體空間可以立即被另作他用。

三、本地方法棧

與虛拟機一樣,本地方法棧區域也會抛出 StackOverflowError 和 OutOfMemeoryError 異常。 

虛拟機棧為虛拟機執行java方法(也就是位元組碼)服務

本地方法棧則為虛拟機使用到的Native方法服務

 與虛拟機一樣,本地方法棧區域也會抛出 StackOverflowError 和 OutOfMemeoryError 異常。

擴充:Native Method:"A native method is a Java method whose implementation is provided by non-java code."

         為什麼有Native Method:有些層次的任務用java實作起來不容易,或者我們對程式的效率很在意時

         ①與java環境外互動

   有時java應用需要與java外面的環境互動。這是本地方法存在的主要原因,你可以想想java需要與一些底層系統如作業系統或某些硬體交換資訊時的情況。本地方法正是這樣一種交流機制:它為我們提供了一個非常簡潔的接口,而且我們無需去了解java應用之外的繁瑣的細節。

        ②與作業系統互動

   JVM支援着java語言本身和運作時庫,它是java程式賴以生存的平台,它由一個解釋器(解釋位元組碼)和一些連接配接到本地代碼的庫組成。然而不管怎 樣,它畢竟不是一個完整的系統,它經常依賴于一些底層(underneath在下面的)系統的支援。這些底層系統常常是強大的作業系統。通過使用本地方法,我們得以用java實作了jre的與底層系統的互動,甚至JVM的一些部分就是用C寫的,還有,如果我們要使用一些java語言本身沒有提供封裝的作業系統的特性時,我們也需要使用本地方法。

       ③Sun's Java

    Sun的解釋器是用C實作的,這使得它能像一些普通的C一樣與外部互動。jre大部分是用java實作的,它也通過一些本地方法與外界互動。例如:類java.lang.Thread 的 setPriority()方法是用java實作的,但是它實作調用的是該類裡的本地方法setPriority0()。這個本地方法是用C實作的,并被植入JVM内部,在Windows 95的平台上,這個本地方法最終将調用Win32 SetPriority() API。這是一個本地方法的具體實作由JVM直接提供,更多的情況是本地方法由外部的動态連結庫(external dynamic link library)提供,然後被JVM調用。

四、Java 堆

      對大部分應用來說,java堆是java虛拟機所管理的記憶體中最大的一塊。java堆是被所有線程共享的一塊記憶體區域(即一個JVM隻有一個堆),在虛拟機啟動時建立。此記憶體區域的唯一目的是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體。

Java 堆可以處于實體上不連續的記憶體空間中,隻要邏輯上是連續即可。在實作時,既可以實作成固定大小的,也可以是可擴充的(通過-Xmx 和 -Xms 控制)。如果在堆中沒有記憶體完成執行個體配置設定,并且堆也無法再擴充時,将會抛出 OutOfMemoryError 異常。

  此外,Java 堆是垃圾收集器器管理的主要區域。大緻可以分成:新生代和老生代,還可再細分。但是進行細分的目的是為了更好地回收記憶體或更快的配置設定記憶體。

四、方法區

      方法區(Method Area)與 Java 堆 一樣,是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編輯器編譯後的代碼等資料。

      方法區是堆的一個邏輯部分,但是它與java堆又是不同的,是以它有了一個别名——非堆(Non-Heap)。

  方法區中的記憶體一般不會被 GC 回收,GC 也難回收,是以被取名為“永久代”,意思是永久存在。這區域的記憶體回收目标主要是針對常量池的回收和對類的解除安裝。但是“永久代”中的資料并非真的永久存在,隻是回收比較麻煩。

  根據 Java 虛拟機規範的規定,當方法區無法滿足記憶體配置設定需求時,将抛出 OutOfMemoryError 異常。