對于一名Java程式開發的開發人員,了解Java的核心記憶體管理,可以令你的薪水有一個突破。
Java程式運作時的資料區可以分為:方法區、堆、虛拟機棧、本地方法棧和程式計數器。其中方法區和堆是由所有線程共享的資料區,後面三個是線程隔離的資料區。
Java虛拟機資料區域
程式計數器
程式計數器是訓示位元組碼執行的一塊小空間,各個功能都需要依靠它,不然就無法實作分支、循環、跳轉、異常處理和線程恢複等。
Java虛拟機的多線程是通過線程輪流切換并配置設定處理器執行時間的方式來實作的,在任何一個确定的時刻,一個處理器(對于多核處理器來說是一個核心)都隻會執行一條線程中的指令。是以,為了線程切換後能恢複到正确的執行位置,每條線程都需要有一個獨立的程式計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類記憶體區域為“線程私有”的記憶體。
如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果正在執行的是Native方法,這個計數器值則為空(Undefined)。此記憶體區域是唯一一個在Java虛拟機規範中沒有規定任何OutOfMemoryError情況的區域。
Java虛拟機棧
Java虛拟機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀,用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊。
每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中入棧到出棧的過程。
局部變量表所需的記憶體空間在編譯期間完成配置設定,其中的内容也在這時期已知,在方法運作期間,局部變量表的内容(支援各種基本資料類型:boolean、byte、char、short、int、float、long、double、對象引用(reference類型)) 可能會變化,但其大小不會變化。
其中64位長度的long和double類型的資料會占用2個局部變量空間(Slot),其餘的資料類型隻占用1個。
如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常;如果虛拟機棧可以動态擴充(目前大部分的Java虛拟機都可動态擴充,隻不過Java虛拟機規範中也允許固定長度的虛拟機棧),如果擴充時無法申請到足夠的記憶體,就會抛出OutOfMemoryError異常。
本地方法棧
本地方法棧和Java虛拟機棧作用十分相似,它們之間的差別不過是虛拟機棧為虛拟機執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛拟機使用到的Native方法服務。
與虛拟機棧一樣,本地方法棧區域也會抛出StackOverflowError和OutOfMemoryError異常。
Java堆
Java堆是虛拟機中最大塊的記憶體,它被所有線程共享,在虛拟機啟動時建立。
Java堆儲存了幾乎所有的對象執行個體,在Java虛拟機規範中的描述為:所有的對象執行個體以及數組都要在堆上配置設定。但是随着JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上配置設定、标量替換優化技術将會導緻一些微妙的變化發生,所有的對象都配置設定在堆上也漸漸變得不是那麼“絕對”了。
Java堆是垃圾收集器管理的主要區域,是以很多時候也被稱做“GC堆”(GarbageCollected Heap)。現在的記憶體回收一般采用了分代收集算法,即分成新生代和老年代,而新生代還分為Eden空間、From Survivor空間、To Survivor空間等。
基于分代的記憶體回收政策的堆空間劃分圖:
Java堆可以處于實體上不連續的記憶體空間中,隻要邏輯上是連續的即可。(通過-Xmx和-Xms控制大小)
方法區
方法區(Method Area)是共享的記憶體區域,存儲着已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。
Java虛拟機規範對方法區的限制非常寬松,除了和Java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充外,還可以選擇不實作垃圾收集。
當方法區無法滿足記憶體配置設定需求時,将抛出OutOfMemoryError異常。
運作時常量池
運作時常量池(Runtime Constant Pool)是方法區的一部分,存放編譯期生成的各種字面量和符号引用,這些内容在類加載後放在此處。
Java語言并不要求常量一定隻有編譯期才能産生,也就是并非預置入Class檔案中常量池的内容才能進入方法區運作時常量池,運作期間也可能将新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。
當常量池無法再申請到記憶體時會抛出OutOfMemoryError異常。
直接記憶體
直接記憶體(Direct Memory)并不是虛拟機運作時資料區的一部分,也不是Java虛拟機規範中定義的記憶體區域。但是這部分記憶體也被頻繁地使用,而且也可能導緻OutOfMemoryError異常出現。
在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回複制資料。