對於從事C、C++程序開發的開發人員來講,在內存管理領域,他們既是擁有最高權力的「皇帝」又是從事最基礎工做的「勞動人民」——既擁有每個對象的「全部權」,又擔負着每個對象生命開始到終結的維護責任。html
對於Java程序員來講,在虛擬機自動內存管理機制的幫助下,再也不須要爲每個new操做去寫配對的delete/free代碼,不容易出現內存泄漏和內存溢出問題,由虛擬機管理內存這一切看起來都很美好。不過,也正是由於Java程序員把內存控制的權力交給了Java虛擬機,一旦出現內存泄漏和溢出方面的問題,若是不瞭解虛擬機是怎樣使用內存的,那麼排查錯誤將會成爲一項異常艱難的工做。java
Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,有些區域則依賴用戶線程的啓動和結束而創建和銷燬。根據《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域。程序員
jdk1.8的時候,方法區(HotSpot 的永久代)被完全移除了(JDK1.7 就已經開始了),取而代之是元空間(Metaspace),元空間使用的是直接內存。算法
在內存區域中,線程共享的部分包括:Java堆、方法區、常量池、直接內存 (非運行時數據區的一部分);線程私有的部分包括:程序計數器、虛擬機棧、本地方法棧。windows
Java 虛擬機所管理的內存中最大的一塊,Java 堆是全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例以及數組都在這裏分配內存。之因此說幾乎是由於有特殊狀況,有些時候小對象會直接在棧上進行分配,這種現象咱們稱之爲棧上分配。數組
Java 堆是垃圾收集器管理的主要區域,所以也被稱做GC 堆(Garbage Collected Heap)。從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此 Java 堆還能夠細分爲:新生代和老年代:再細緻一點,年輕代能夠劃分爲:Eden 空間、From Survivor (S0)、To Survivor (S1) 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。oracle
從上圖能夠看出,堆大小 = 新生代 + 老年代。其中,堆的大小能夠經過參數 –Xms、-Xmx 來指定。函數
根據 java官方文檔(JDK8)能夠看出:性能
默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值爲 1:2 ( 該值能夠經過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。其中,新生代 ( Young ) 被細分爲 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名爲 From 和 To,以示區分。spa
默認的,Eden : From : To = 8 : 1 : 1 ( 能夠經過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。
JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來爲對象服務,因此不管何時,老是有一塊 Survivor 區域是空閒着的。所以,新生代實際可用的內存空間爲 9/10 ( 即90% )的新生代空間。
大部分狀況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,若是對象還存活,則會進入 S0 或者 S1,而且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增長到必定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold 來設置。
方法區與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作Non-Heap(非堆),目的應該是與Java堆區分開來。
對於習慣在HotSpot虛擬機上開發、部署程序的開發者來講,不少人都更願意把方法區稱爲「永久代」(Permanent Generation),本質上二者並不等價,僅僅是由於HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已,這樣HotSpot的垃圾收集器能夠像管理Java堆同樣管理這部份內存,可以省去專門爲方法區編寫內存管理代碼的工做。對於其餘虛擬機(如BEA JRockit、IBM J9等)來講是不存在永久代的概念的。
方法區和永久代的關係很像 Java 中接口和類的關係,類實現了接口,而永久代就是 HotSpot 虛擬機對虛擬機規範中方法區的一種實現方式。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機規範中的定義,是一種規範,而永久代是一種實現。
JDK 1.8 以前永久代還沒被完全移除的時候一般經過下面這些參數來調節方法區大小
-XX:PermSize=N //方法區 (永久代) 初始大小
-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen
相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入方法區後就「永久存在」了。
JDK 1.8 的時候,方法區(HotSpot 的永久代)被完全移除了(JDK1.7 就已經開始了),取而代之是元空間(MetaSpace)。
替換的緣由是:
-XX:MaxMetaspaceSize
標誌設置最大元空間大小,默認值爲 unlimited,這意味着它只受系統內存的限制。-XX:MetaspaceSize
調整標誌定義元空間的初始大小若是未指定此標誌,則 Metaspace 將根據運行時的應用程序需求動態地從新調整大小。另:永久代被移除後,方法區移至Metaspace,字符串常量移至Java Heap。
下面是一些經常使用參數:
-XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小
與永久代很大的不一樣就是,若是不指定大小的話,隨着更多類的建立,虛擬機會耗盡全部可用的系統內存。
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。
JDK1.7 及以後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。
程序計數器是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。
若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Native方法,這個計數器值則爲空(Undefined)。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。
與程序計數器同樣,Java虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
Java 內存能夠粗糙的區分爲堆內存(Heap)和棧內存 (Stack),其中棧就是如今說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。局部變量表存放了編譯期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。
在Java虛擬機規範中,對這個區域規定了兩種異常情況:
本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。
與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
直接內存並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。可是這部份內存也被頻繁地使用,並且也可能致使OutOfMemoryError異常出現。
在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。
本機直接內存的分配不會收到 Java 堆的限制,可是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。所以,若是在配置虛擬機參數時忽略了其直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),會致使動態擴展時出現OutOfMemoryError異常。