1.1 概述
對于Java程序員來說, 在虛擬機自動內存管理機制的幫助下, 不再需要為每一個new操作去寫配對的delete/free代碼, 不容易出現內存泄漏和內存溢出的問題, 由虛擬機管理內存這一切看起來都十分美好. 不過, 也正是因為Java程序員把內存控制的權利交給了Java虛擬機, 一旦出現內存泄露和溢出方面的問題, 如果不了解虛擬機是怎樣使用內存的, 那么排查錯誤將會成為一項異常艱難的工作.
1.2 運行時數據區域
Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域, 這些數據區域都有各自不同的用途, 以及創建和銷毀的時間, 有的區域隨著虛擬機進程的啟動而存在, 有些區域則依賴用戶線程的啟動和結束而建立和銷毀. 根據《Java虛擬機規范(Java SE7版)》的規定, Java虛擬機所管理的內存將會包括以下幾個運行時數據區域. 如圖
1.2.1 程序計數器
程序計數器(Program Counter Register)是一塊較小的內存空間, 它可以看作是當前線程所執行的字節碼行號指示器. 在虛擬機的概念模型里(僅是概念模型, 各種虛擬機可能會通過一些更高效的方式實現), 字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令, 分支, 循環, 跳轉, 異常處理, 線程恢復等基礎功能都需要依賴這個計數器來完成.
由于Java虛擬機的多線程的實現是通過線程輪流切換并分配處理器執行時間的方式來實現的, 在任何一個確定的時間, 一個處理器(對于多核處理器來說是一個內核)都只會執行一條線程中的指令. 因此, 為了線程切換后能恢復到正確的執行位置, 每條線程都需要有一個獨立的程序計數器, 各條線程之間的計數器互相不受影響, 獨立存儲, 我們稱這類內存區域為"線程私有"的內存.
如果線程正在執行的是一個Java方法, 這個計數器記錄的是正在執行的虛擬機字節碼指令的地址; 如果正在執行的是Native方法, 這個計數器值則為空(Undefined). 此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError
情況的區域.
1.2.2 Java虛擬機棧
與程序計數器一樣, Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的, 它的生命周期與線程相同. 虛擬機棧描述的是Java方法執行的內存模型: 每個方法在執行的同時都會創建一個棧幀(Stack Frame)用于存儲局部變量表, 操作數棧, 動態鏈接, 方法出口等信息. 每一個方法從調用直至執行完成的過程, 就對應一個棧幀在虛擬機棧中入棧與出棧的過程.
經常有人把Java內存分為堆內存(Heap)和棧內存(Stack), 這種方法比較粗糙, Java內存區域的劃分實際上遠比這復雜. 這種劃分方式的流行只能說明大多數程序員最關注的, 與對象內存分配關系最密切的內存區域是這兩塊. 其中所指的"堆"會在后面專門講述, 而所指的"棧"就是現在所講的虛擬機棧, 或者說是虛擬機棧中局部變量表部分.
局部變量表存放了編譯期可知的各種基本數據類型(boolean, byte, char, short, int, float, long, double), 對象引用(reference類型, 它不等同于對象本身, 可能是一個指向對象起始地址的引用指針, 也可能是指向一個代表對象句柄或其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址).
其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot), 其余的數據類型之占用1個. 局部變量表所需的內存空間在編譯期完成分配, 當進入一個方法時, 這個方法需要在棧中分配多大的局部變量空間是完全確定的, 在方法運行期間是不會改變局部變量表的大小.
在Java虛擬機規范中, 對這個區域規定了兩種異常狀況: 如果線程請求的棧深度大于虛擬機所允許的深度, 將拋出StackOverfloatError
異常; 如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可以動態擴展, 只不過Java虛擬機規范中也允許固定長度的虛擬機棧), 如果擴展時無法申請足夠的內存, 就會拋出OutOfMemoryError
異常.
1.2.3 本地方法棧
本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用非常相似, 它們的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務, 而本地方法棧則為虛擬機使用到的Native方法服務. 在虛擬機規范中對本地方法棧中方法是用的語言, 使用方法與數據結構并沒有強制規定, 因此具體的虛擬機可以自由實現它. 甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一. 與虛擬機棧一樣, 本地方法棧區域也會拋出StackOverfloorError
和OutOfMemoryError
異常.
1.2.4 Java堆
對于大多數應用來說, Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊. Java堆是被所有線程共享的一塊內存區域, 在虛擬機啟動時創建. 此內存區域的唯一目的就是存放對象實例, 幾乎所有的對象實例都是在這里分配內存. 這一點在Java虛擬機規范中描述是: 所有的對象實例以及數組都要在堆上分配, 但隨著JIT編譯器的發展與逃逸分析技術逐漸成熟, 棧上分配, 標量替換優化技術將會導致一些微妙的變化發生, 所有的對象都分配在堆上也漸漸變得不是那么"絕對"了.
Java堆是垃圾收集器管理的主要區域, 因此很多時候也被稱作"GC堆"(Garbage Collected Heap), 從內存回收的角度來看, 由于現有的收集器基本都采用分代收集算法,所以Java堆還可以細分為: 新生代和老年代; 再細致一點的有Eden空間, From Survivor空間, To Survivor空間等. 從內存分配的角度來看, 線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer, TLAB). 不過無論如何劃分, 都與存放內容無關, 無論哪個區域, 存儲的都是對象實例, 進一步劃分的目的是為了更好地回收內存, 或者更快地分配內存.
根據Java虛擬機規范的規定, Java堆可以處于物理上不連續的內存空間中, 只要邏輯上是連續的即可, 就像我們的磁盤空間一樣. 在實現時, 既可以實現成固定大小的, 也可以是可擴展的, 不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制). 如果在堆上沒有內存完成實例分配, 并且堆也無法擴展時, 將拋出OutOfMemoryError
異常.
1.2.5 方法區
方法區(Method Area)與Java堆一樣, 是各個線程共享的內存區域, 它用于存儲已被虛擬機加載的類信息, 常量, 靜態變量, 即時編譯器編譯后的代碼等數據. 雖然Java虛擬機規范把方法區描述為堆上的一個邏輯部分, 但是它卻有一個別名叫做Non-Heap(非堆), 目的應該是與Java堆區分開來.
Java虛擬機規范對方法區的限制非常寬松, 除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外, 還可以選擇不實現垃圾收集. 相對而言, 垃圾收集行為在這個區域是比較少出現的, 但并非數據進入了方法區就如永久代的名字一樣"永久"存在了.這區域的內存回收目標主要是針對常量池的回收和對類型的卸載, 一般來說, 這個區域的回收"成績"比較難以令人滿意, 尤其是類型的卸載, 條件相當苛刻, 但是這部分區域的回收確實是必要的.
根據Java虛擬機規范的規定, 當方法區無法滿足內存分配的需求時, 將拋出OutOfMemoryError
異常.
1.2.6 運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分. Class文件中除了有類的版本, 字段, 方法, 接口等描述信息外, 還有一項信息是常量池(Constant Pool Table), 用于存放編譯期生成的各種字面量和符號引用, 這部分內容將在類加載后進入方法區的運行時常量池中存放.
Java虛擬機對Class文件的每一部分(也包括常量池)的格式都有嚴格規定, 每一個字節用于存儲哪種數據類型都必須符合規范上的要求才會被虛擬機認可, 裝載和執行, 但對于運行時常量池, Java虛擬機規范沒有做任何細節的要求, 不同的提供商實現的虛擬機可以按照自己的需求來實現這個內存區域. 不過, 一般來說, 除了保存Class文件中描述的符號引用外, 還會把翻譯出來的直接引用也存儲在運行時常量池中.
運行時常量池相對于Class文件常量池的另外一個重要特征是具備動態性, Java語言并不要求常量一定只有編譯期才能產生, 也就是并非預置入Class文件中常量池的內容才能進入方法區運行時常量池, 運行期間也可能將新的常量放入池中, 這種特性被開發人員利用得比較多的便是String類的intern()方法.
既然運行時常量池是方法區的一部分, 自然受到方法區內存的限制, 當常量池無法再申請到內存時會拋出OutOfMemoryError
異常.
1.2.7直接內存
直接內存(Direct Memory)并不是虛擬機運行時數據區的一部分, 也不是Java虛擬機規范中定義的內存區域. 但是這部分內存也被頻繁地使用, 而且也可能導致OutOfMemoryError
異常出現.
在JDK 1.4中新加入了NIO(New Input/Output)類, 引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式, 它可以使用Native函數庫直接分配堆內存, 然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作. 這樣能在一些場景中顯著提高性能, 因為避免了在Java堆和Native堆中來回復制數據.
顯然, 本機直接內存的分配不會受到Java堆大小的限制, 但是, 既然是內存, 肯定還是受到本機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制. 服務器管理員在配置虛擬機參數時, 會根據實際內存設置-Xmx等參數信息, 但經常忽略直接內存, 使得各個內存區域總和大于物理內存限制(包括物理的和操作系統級別的限制), 從而導致動態擴展時出現OutOfMemoryError
異常.