天天看點

JVM記憶體區域介紹JVM記憶體區域介紹

JVM記憶體區域介紹

    衆所周知,對于C/C++程式員來說,他們即擁有記憶體管理上的最高權力, 也需要對一個對象從建立到消亡的整個生命周期負責到底。然而對于Java程式員來說,通常情況下我們無需對記憶體管理進行幹預,底層虛拟機的垃圾回收機制和記憶體配置設定算法很好的管理了對象的建立和消亡過程。但是如果我們了解一些Java底層虛拟機記憶體配置設定機制,對解決程式記憶體方面遇到的一些異常是會有很大幫助的。

    Java虛拟機在執行java程式時,将其管理的記憶體分成了不同的區域,這些區域有着不同的用途、不同的建立時間和銷毀時間。有些區域随着java虛拟機程序的啟動而存在,有些則随着使用者線程的啟動而建立,線程結束而銷毀。根據java虛拟機規範裡的描述,Java虛拟機記憶體區域大體分為如下幾塊:

JVM記憶體區域介紹JVM記憶體區域介紹

程式計數器

    程式計數器是虛拟機記憶體區域中占用很小的一塊區域, 是線程私有的,跟随線程的生命周期。

    由于java虛拟機的多線程是通過線程輪流切換并配置設定處理器執行時間來實作的,在任何時刻對于一個處理器(對于多核處理器來說是一個核心)而言隻能執行一個線程的指令,線上程切換的時候其實是通過各自的程式計數器中的資訊來記錄目前執行位置的。是以說,程式計數器是線程的私有财産。

    如果線程執行的是java方法,則程式計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果執行的是native方法, 則計數器的值是undefined。此記憶體區域是唯一一個在java虛拟機規範中沒有規定任何OutOfMemoryError情況的區域。

Java虛拟機棧

    Java虛拟機棧和程式計數器一樣,也是線程私有的,生命周期跟随線程。虛拟機棧儲存的是java方法的記憶體模型:每個方法在執行時都會建立一個棧幀,用來儲存局部變量表、操作數棧、動态連結庫、方法出口等資訊。每個方法的執行都對應一個棧幀從java虛拟機棧中入棧和出棧的過程。

     有人把java虛拟機記憶體劃分為堆和棧這兩大塊,其實java虛拟機記憶體區域劃分遠比這複雜得多, 隻能說大部分程式員隻關注到了他們常用的記憶體區域。其中的“棧”其實隻能說是我們現在在介紹的Java虛拟機棧,或者說是Java虛拟機棧中的局部變量表的部分。

    局部變量表存放了編譯期間各種基本資料類型(byte、short、int、long、float、double、char、boolean)、對象引用(reference類型,它不等同于對象本身,可能是一個指向對象起始位址的引用指針, 也可能是對象的一個句柄或者其他與此對象相關的位置)和returAddress類型(指向了一條位元組碼指令的位址)。

    其中64位的long和double類型的資料占用2個局部變量的空間,其他類型隻占用1個。局部變量表所需的記憶體空間在編譯時需要完成配置設定,當進入一個方法時,這個方法需要在幀上配置設定多大的局部變量空間是早就确定的,在運作期間不會改變局部變量表的空間大小。

    Java虛拟機規範中對該記憶體區域定義了兩種異常情況:如果線程請求的棧的深度大于目前的深度,則虛拟機會抛出棧溢出異常(StackOverflowError); 如果虛拟機棧允許動态擴充, 當擴充時若記憶體不足則會抛出記憶體溢出異常(OutOfMemoryError)。

本地方法棧

    本地方法棧和java虛拟機棧其實是完全一樣的,隻不過是Java虛拟機棧中針對是java的方法(位元組碼)服務的,而本地方法棧是java調用native方法服務的。Java虛拟機沒有規定本地方法棧的語言、使用方式和資料結構,是以不同的虛拟機可以對其進行不同的實作。甚至有些虛拟機(比如Sun的hot spot虛拟機)就将本地方法棧和java虛拟機棧合二為一了。本地方法棧也會抛出OutofMemoryError和StackOverflowError。

Java堆

    對于大多數Java應用來說,Java堆是Java虛拟機管理的最大的一塊記憶體區域了。Java堆是被所有記憶體共享的一塊區域, 在虛拟機程序建立的時候建立。此記憶體區域的唯一目的是存放對象執行個體,幾乎所有的對象和數組都在堆上被建立和配置設定記憶體。Java虛拟機規範中聲稱:所有的對象和數組執行個體對象都在Java堆上完成建立和配置設定。其實随着JIT編譯器的發展和逃逸分析技術的逐漸成熟,棧上配置設定、标量替換優化技術将會導緻一些微妙的變化發生,所有的對象都配置設定在堆上也漸漸變得不是那麼絕對了。

    Java堆是垃圾收集器管理的主要區域, 簡稱GC堆。根據垃圾收集器的分代收集算法,可以将GC堆細分為新生代、老年代。更細一點則劃分為Eden空間、From Survivor空間和To Survivor空間等。從記憶體配置設定的角度,Java堆還可能劃分出多個線程私有的配置設定緩沖區(Tread Local Allocation Buffer 簡稱TLAB),這隻是為了友善垃圾收集器進行更好更快的記憶體回收管理,對具體存放内容無任何影響,存放的仍是對象執行個體。

    根據Java虛拟機規範的規定: Java堆可以存在不連續的記憶體空間中,隻要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實作上,可以是固定大小,也可以是可擴充的。目前大多數Java虛拟機都将Java堆空間做成可擴充的了(通過-Xmx和-Xms實作)。當對象執行個體建立時若空間不夠則抛出OutofMemoryError。

方法區

    方法區和堆一樣,也是線程共享的記憶體區域, 用于存儲已被虛拟機加載的類資訊、常量、靜态常量、即時編譯器變異後的代碼等資料。雖然Java虛拟機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個别名叫做(非堆), 目的應該是與Java堆區分開來。

    對于習慣在HotSpot虛拟機下開發和部署程式的開發者來說,更願意把方法區稱作永久代。本質上兩者并不等價,僅僅是因為HotSpot的設計團隊将GC分代收集擴充到了方法區, 更加,或者說使用永久代來實作方法區而已,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分記憶體,能夠省去專門為方法區編寫記憶體管理代碼的工作。對于其他虛拟機(如BEA JRockit、IBM J9等)來說是不存在永久代的概念的。原則上,如何實作方法區屬于虛拟機實作細節,不受虛拟機規範限制,但使用永久代來實作方法區,現在看來并不是一個好主意,因為這樣更容易遇到記憶體溢出問題(永久代有-XX:MaxPermSize的上限,J9和JRockit隻要沒有觸碰到程序可用記憶體的上限,例如32位系統中的4GB,就不會出現問題),而且有極少數方法(例如String.intern())會因這個原因導緻不同虛拟機下有不同的表現。是以,對于HotSpot虛拟機,根據官方釋出的路線圖資訊,現在也有放棄永久代并逐漸改為采用Native Memory來實作方法區的規劃了,在目前已經釋出的JDK 1.7的HotSpot中,已經把原本放在永久代的字元串常量池移出。

    Java虛拟機規範對方法區的限制非常寬松,除了和Java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充外,還可以選擇不實作垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的,但并非資料進入了方法區就如永久代的名字一樣“永久”存在了。這區域的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝,一般來說,這個區域的回收“成績”比較難以令人滿意,尤其是類型的解除安裝,條件相當苛刻,但是這部分區域的回收确實是必要的。在Sun公司的BUG清單中,曾出現過的若幹個嚴重的BUG就是由于低版本的HotSpot虛拟機對此區域未完全回收而導緻記憶體洩漏。

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

運作時常量區

     運作時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符号引用,這部分内容将在類加載後進入方法區的運作時常量池中存放。

    Java虛拟機對Class檔案每一部分(自然也包括常量池)的格式都有嚴格規定,每一個位元組用于存儲哪種資料都必須符合規範上的要求才會被虛拟機認可、裝載和執行,但對于運作時常量池,Java虛拟機規範沒有做任何細節的要求,不同的提供商實作的虛拟機可以按照自己的需要來實作這個記憶體區域。不過,一般來說,除了儲存Class檔案中描述的符号引用外,還會把翻譯出來的直接引用也存儲在運作時常量池中。

    運作時常量池相對于Class檔案常量池的另外一個重要特征是具備動态性,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堆中來回複制資料。

    顯然,本機直接記憶體的配置設定不會受到Java堆大小的限制,但是,既然是記憶體,肯定還是會受到本機總記憶體(包括RAM以及SWAP區或者分頁檔案)大小以及處理器尋址空間的限制。伺服器管理者在配置虛拟機參數時,會根據實際記憶體設定-Xmx等參數資訊,但經常忽略直接記憶體,使得各個記憶體區域總和大于實體記憶體限制(包括實體的和作業系統級的限制),進而導緻動态擴充時出現OutOfMemoryError異常。

注: 本文基本來源于周志明老師的書籍《深入了解Java虛拟機》