1 討論背景
周志明老師寫的《深刻理解Java虛擬機》應該不少程序員都讀過,第二章中闡述了Java虛擬機在執行Java程序的過程當中是如何管理內存的,以及這些內存是如何被劃分紅更細的邏輯區域的。以下圖所示,按照書中的論述JVM運行時數據區域包含如下幾個數據區[1]。java
按照《Java虛擬機規範(Java SE 7版)》,各區域的功能簡要介紹以下:程序員
- 程序計數器:各線程私有。用於記錄每一個線程下一條待執行的字節碼指令以及相關信息。這是惟一的不會拋出OOM異常的區域。
- Java虛擬機棧:各線程私有。虛擬機棧由一個個的棧幀組成,每一個棧幀包含了對應方法執行所須要的信息,具體包括:局部變量表、操做數棧(相似於編譯型語言體系下的數據寄存器)、動態連接(某些接口符號可能會動態的指向不一樣的目標方法)、函數返回地址以及其餘一些相關信息。理論上當函數調用鏈超過棧的深度時就會觸發StackOverflow,當該區域設置爲動態擴展時,虛擬機沒法爲棧申請到更多內存時就會觸發OOM。事實中基本上無論哪一種狀況,結果都極可能會是StackOverflow,由於棧容量和棧幀的大小決定了棧的深度(棧幀大小*深度<=棧容量),因此當OOM時,棧深度必定也已經不夠用了,因此拋出StackOverflow異常也無可厚非。能夠經過「-Xss」來配置虛擬機棧固定大小。
- Java堆:各線程公有。虛擬機工做的主要內存區域(大部分狀況下也是最大的),絕大部分對象實例的內存分配都在這裏進行。Java 7和以前的Java堆細分爲:新生代(伊甸區、存活區0、存活區1)、年老代和永久代。Java 8去除了永久代,替換以Metaspace。在JVM的運行中,大部分狀況下,GC主要就發生在堆區域,
- 方法區:各線程公有。用於存放類定義、常量池、靜態變量(static修飾)、編譯後的字節碼等。方法區其實是從堆上劃分出來的一塊區域,可是其GC機制是單獨的,與堆不一樣,因此爲了區分方法區和堆,一般又把方法區叫作「非堆」。方法區對應了堆中的永久代。所以在Java8以及以後版本中,永久代被抹除了,方法區也移到了元數據空間(metaspace)中。
- 運行時常量池:各線程公有。用於存放類信息中的常量(字面量、符號引用等),每一個類編譯後的信息中的都有一個常量池,能夠經過javap -vebose xxxx.class命令來查看。
- 直接內存:進程間公有。直接內存不屬於Java虛擬機運行時數據區的一部分,它是指操做系統分配給虛擬機以及其餘進程所運行的那塊內存區域,之因此這麼說,是由於不少服務器都是虛擬機(操做系統級別),對於物理機來講,這塊內存就是指操做系統所管控的物理內存。經過在堆中建立一個DirectByteBuffer實例來對直接內存進行訪問。
不少讀者瞭解完這些後仍是雲裏霧裏,各論壇仍是會出現各類沒有定論的問題,好比服務器
- 字符串常量池屬於哪一個數據區?書中對字符串常量池和運行時常量池描述的至關晦澀和模糊。
- Java六、Java7和Java8的運行時內存數據區域到底有何不同?
- 什麼是字面量,什麼又是字符串常量?
- 什麼是本地內存?他和直接內存相同嘛?什麼又是堆外內存?
下面咱們圍繞這幾個問題作一些討論和引伸,從而幫助咱們更好的理解運行時數據區域劃分。數據結構
2 字符串常量池
咱們先來回答第一和第二個問題。app
2.1 字符串常量池在哪
在不一樣的Java版本中,規範規定的字符串常量池的位置也不同。如下三張圖分別表明了Java六、Java7和Java8體系下的Java虛擬機與運行時數據區域劃分,哪些是線程私有,哪些是線程公有,哪些又是進程間公有都比較清晰了。函數
2.1.1 Java 6 虛擬機運行數據區
當咱們聽到「字符串常量池也是方法區的一部分」的時候,咱們要知道他大概暗指的是Java 6或者以前的版本。如上圖所示,在Java 6虛擬機規範中,字符串常量池確實是方法區的一部分,受永久代內存區大小的限制。當頻繁使用Spring.intern()時,可能會引起OOM(PermGen space)。ui
2.1.2 Java 7 虛擬機運行數據區
從Java 7 開始,規範將字符串常量池遷移到了Java堆中,受Java堆大小的限制。當頻繁大量使用String.intern()時,可能會引起OOM(Java heap space)。spa
2.1.3 Java 8 虛擬機運行數據區
Java 8 虛擬機規範完全移除了永久代(-XX:Permsize和-XX:MaxPermsize均已失效),替而代之的則是元空間(Metaspace)。字符串常量池仍然在Java堆中,但方法區已經遷移到了元空間中。這時候因爲濫用 String.intern()引起的OOM依舊在Java堆中。操作系統
2.2 字符串常量池是啥
那麼字符串常量池的數據結構是怎麼實現的呢?答案是HashMap,每一個字符串常量池對應了一個StringTable的數據結構,其本質並非Table,而是一個HashMap。這個HashMap的容量是固定的(默認1009),能夠經過-XX:StringTableSize來設置,注意這個值是指哈希表中桶的數量,不是佔用內存的大小。因此這個值最好是一個質數,而且要大於默認的1009[2]。線程
3 字面量和字符串常量
如如下代碼:
String str = "123";
其中」123」就是咱們常常看到的「字面量」。字面量是隨着Class信息等在類被加載完畢後一塊兒進入運行時常量池的。 而
String str2 = str.intern();
這句代碼則嘗試將str的值放入字符串常量池,然而」123」已經在類信息的常量池中了,因此StringTable實際記錄的是類信息常量池中該字符串的引用。
對於語句:
String str = new StringBuilder("hello").append(" world").toString().intern();
這會將新建立的「hello world」的堆內對象引用(str)放入到字符串常量池中,由於這是第一次出現,沒有其餘地方存在該值的引用。
4 本地內存和直接內存
首先須要說明的是,本地內存(Native Memory)和堆外內存(Off-heap Memory)的含義是同樣的。而關於直接內存和本地內存的關係,StackOverflow上也沒有說清楚的帖子,第二部分中的三張圖已經能夠很好的說明直接內存和本地內存的關係了,所謂的本地內存是操做系統分配給JVM虛擬機(做爲一個進程)使用的內存塊中除去堆的那一部分。而直接內存則是全部進程共享的操做系統所控制的內存。因此能夠這麼說:本地內存和直接內存的關係就像「蘋果」和「水果」的關係,蘋果屬於水果,是水果更具體的限定。Java8中的元空間就屬於本地內存空間,而他們都是直接內存的一部分。 經過DirectByteBuffer分配的內存區域必定在本地內存中,它也受直接內存大小的限制。本地內存的大小也有限制,好比Window中對每一個程序運行所需的內存大小作了2G的默認限制,這隻時候其上運行的JVM的本地內存大小≈2G-JVM堆內存大小。
5 字符串常量池所屬數據區的具體說明
下面咱們舉2個例子討論下在Java6和Java7(含以後版本)下字符串常量池遷移帶來的變化
5.1 例子1
請給出如下代碼拋出異常的類型:
import java.util.ArrayList; import java.util.List; public class Test { public static void main(String[] args){ List<String> list = new ArrayList<String>(); int i = 0; while(true) { list.add( String.valueOf(i++).intern()); } } }
而後啓動參數中咱們加上:
-XX:PermSize=10M -XX:MaxPermSize=10M
分析下這個代碼,其意圖在於不斷的產生新的字符串,而且放入字符串常量池中,試圖撐爆永久代。然而這隻會在Java 6 中發生,對於Java7和Java8來講,字符串常量池已經遷移到了Java堆中,若是這時候咱們添加如下虛擬機參數:
-Xms10M -Xmx10M
則會引起:java.lang.OutOfMemoryError: GC overhead limit exceeded 這樣的錯誤,這個異常的本質與 OOM(Heap space)一直,都是堆內存溢出。
5.2 例子2
如下代碼在Java6和Java7中輸出也不相同:
public class TestStringConstantPool { public static String hello = "Hello Java"; public static void main(String[] args) { String str1 = new StringBuilder("Hello ").append("World").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("Hello ").append("Java").toString(); System.out.println(str2.intern() == str2); } }
在Java6中會輸出:
false false
在Java7中則輸出:
true false
首先咱們分析下Java6中的場景,Java6中字符串常量池仍是運行時常量池的一部分,因此使用String.intern()時,會把堆中的字符串複製到方法區中,返回的是方法區中的對象引用。因此無論如何,堆中對象和方法區中對象應用都不會想等。 而在Java7中,這個狀況發生了變化,字符串常量池轉移到了堆中,對於str1來講,字符串常量池StringTable會記錄其在堆中的引用(即str1)。因此str1.intern() == str1成立。而str2狀況則不同了,由於「Hello Java」字符串已經存在於方法區的運行時常量池中,因此intern()返回的是方法區中的對象引用。因此str2.intern() == str2不成立。