/**<br />JVM執行Java程式的過程中,會使用到各種資料區域,這些地區有各自的用途、建立和銷毀時間。根據《Java虛擬機器規範(第二版)》(下文稱VM Spec)的規定,JVM包括下列幾個運行時資料區域:<br /> **/</p><p>/**<br />1.程式計數器(Program Counter Register):</p><p>每一個Java線程都有一個程式計數器來用於儲存程式執行到當前方法的哪一個指令,對於非Native方法,這個地區記錄的是正在執行的VM原語的地址,</p><p>如果正在執行的是Natvie方法,這個地區則為空白(undefined)。此記憶體地區是唯一一個在VM Spec中沒有規定任何OutOfMemoryError情況的地區。<br />**/</p><p>/**<br />2.Java虛擬機器棧(Java Virtual Machine Stacks)</p><p>與程式計數器一樣,VM棧的生命週期也是與線程相同。VM棧描述的是Java方法調用的記憶體模型:每個方法被執行的時候,都會同時建立一個幀(Frame)用於儲存本地變數表、操作棧、動態連結、方法出入口等資訊。</p><p>每一個方法的調用至完成,就意味著一個幀在VM棧中的入棧至出棧的過程。在後文中,我們將著重討論VM棧中本地變數表部分。</p><p>經常有人把Java記憶體簡單的區分為堆記憶體(Heap)和棧記憶體(Stack),實際中的地區遠比這種觀點複雜,這樣劃分只是說明與變數定義密切相關的記憶體地區是這兩塊。</p><p>其中所指的“堆”後面會專門描述,而所指的“棧”就是VM棧中各個幀的本地變數表部分。本地變數表存放了編譯期可知的各種標量類型(boolean、byte、char、short、int、float、long、double)、對象引用(不是對象本身,僅僅是一個引用指標)、方法返回地址等。</p><p>其中long和double會佔用2個本地變數空間(32bit),其餘佔用1個。本地變數表在進入方法時進行分配,當進入一個方法時,這個方法需要在幀中分配多大的本地變數是一件完全確定的事情,在方法運行期間不改變本地變數表的大小。</p><p>在VM Spec中對這個地區規定了2中異常狀況:如果線程請求的棧深度大於虛擬機器所允許的深度,將拋出StackOverflowError異常;如果VM棧可以動態擴充(VM Spec中允許固定長度的VM棧),當擴充時無法申請到足夠記憶體則拋出OutOfMemoryError異常。<br /> **/</p><p>/**<br />3.本地方法棧(Native Method Stacks)</p><p>本地方法棧與VM棧所發揮作用是類似的,只不過VM棧為虛擬機器運行VM原語服務,而本地方法棧是為虛擬機器使用到的Native方法服務。</p><p>它的實現的語言、方式與結構並沒有強制規定,甚至有的虛擬機器(譬如Sun Hotspot虛擬機器)直接就把本地方法棧和VM棧合二為一。</p><p>和VM棧一樣,這個地區也會拋出StackOverflowError和OutOfMemoryError異常。<br /> **/</p><p>/**<br />4.Java堆(Java Heap)</p><p>對於絕大多數應用來說,Java堆是虛擬機器管理最大的一塊記憶體。Java堆是被所有線程共用的,在虛擬機器啟動時建立。</p><p>Java堆的唯一目的就是存放對象執行個體,絕大部分的對象執行個體都在這裡分配。</p><p>這一點在VM Spec中的描述是:所有的執行個體以及數組都在堆上分配(原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated),但是在逃逸分析和標量替換最佳化技術出現後,VM Spec的描述就顯得並不那麼準確了。</p><p>Java堆內還有更細緻的劃分:新生代、老年代,再細緻一點的:eden、from survivor、to survivor,甚至更細粒度的本地線程分配緩衝(TLAB)等,無論對Java堆如何劃分,目的都是為了更好的回收記憶體,或者更快的分配記憶體,在本章中我們僅僅針對記憶體地區的作用進行討論,Java堆中的上述各個地區的細節,可參見本文第二章《JVM記憶體管理:深入垃圾收集器與記憶體配置策略》。</p><p>根據VM Spec的要求,Java堆可以處於物理上不連續的記憶體空間,它邏輯上是連續的即可,就像我們的磁碟空間一樣。實現時可以選擇實現成固定大小的,也可以是可擴充的,不過當前所有商業的虛擬機器都是按照可擴充來實現的(通過-Xmx和-Xms控制)。</p><p>如果在堆中無法分配記憶體,並且堆也無法再擴充時,將會拋出OutOfMemoryError異常。<br /> **/</p><p>/**<br />5.方法區(Method Area)</p><p>叫“方法區”可能認識它的人還不太多,如果叫永久代(Permanent Generation)它的粉絲也許就多了。</p><p>它還有個別名叫做Non-Heap(非堆),但是VM Spec上則描述方法區為堆的一個邏輯部分(原文:the method area is logically part of the heap),這個名字的問題還真容易令人產生誤解,我們在這裡就不糾結了。</p><p>方法區中存放了每個Class的結構資訊,包括常量池、欄位描述、方法描述等等。VM Space描述中對這個地區的限制非常寬鬆,除了和Java堆一樣不需要連續的記憶體,也可以選擇固定大小或者可擴充外,甚至可以選擇不實現垃圾收集。</p><p>相對來說,垃圾收集行為在這個地區是相對比較少發生的,但並不是某些描述那樣永久代不會發生GC(至少對當前主流的商業JVM實現來說是如此),這裡的GC主要是對常量池的回收和對類的卸載,雖然回收的“成績”一般也比較差強人意,尤其是類卸載,條件相當苛刻。<br /> **/</p><p>/**<br />6.運行時常量池(Runtime Constant Pool)</p><p>Class檔案中除了有類的版本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量表(constant_pool table),用於存放編譯期已可知的常量,這部分內容將在類載入後進入方法區(永久代)存放。但是Java語言並不要求常量一定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,運行期間也可將新內容放入常量池(最典型的String.intern()方法)。</p><p>運行時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法在申請到記憶體時會拋出OutOfMemoryError異常。<br /> **/</p><p>/**<br />7.本機直接記憶體(Direct Memory)</p><p>直接記憶體並不是虛擬機器運行時資料區的一部分,它根本就是本機記憶體而不是VM直接管理的地區。但是這部分記憶體也會導致OutOfMemoryError異常出現,因此我們放到這裡一起描述。</p><p>在JDK1.4中新加入了NIO類,引入一種基於渠道與緩衝區的I/O方式,它可以通過本機Native函數庫直接分配本機記憶體,然後通過一個儲存在Java堆裡面的DirectByteBuffer對象作為這塊記憶體的引用進行操作。這樣能在一些情境中顯著提高效能,因為避免了在Java對和本機堆中來回複製資料。</p><p>顯然本機直接記憶體的分配不會受到Java堆大小的限制,但是即然是記憶體那肯定還是要受到本機實體記憶體(包括SWAP區或者Windows虛擬記憶體)的限制的,一般伺服器管理員配置JVM參數時,會根據實際記憶體設定-Xmx等參數資訊,但經常忽略掉直接記憶體,使得各個記憶體地區總和大於實體記憶體限制(包括物理的和作業系統級的限制),而導致動態擴充時出現OutOfMemoryError異常。<br /> **/