橙色表明java虛擬機運行時候的線程共享的數據區域,綠色表明了運行時候線程的數據隔離區域。java
程序計數器佔用一小塊內存區域,能夠看作是當前線程所執行的字節碼的行號指示器,字節碼解釋器工做的時候就是經過改變這個程序計數器的值來選擇下一條須要執行的字節碼指令。分支,循環,跳轉,異常處理,線程恢復等基礎功能都須要依賴程序計數器來執行。程序員
若是一個線程正在執行java方法,則這個計數器記錄的正在執行的虛擬機字節碼的指令地址,若是是一個native方法在執行,那麼程序計數器的值爲空(undifined)。此內存區域是惟一一個在java虛擬機規範中沒有規定任何內存溢出狀況的區域。數組
Java虛擬機棧是線程私有的,他的生命週期和線程生命週期同樣,虛擬機棧描述的是Java方法執行的內存模型,每一個方法執行的同時會建立一個棧幀,用於存儲局部變量表,操做數棧,動態連接,方法出口等等信息。每一個方法調用執行完的過程,對應着棧針在虛擬機棧中入棧到出棧的過程。安全
Java內存區域的劃分主要分爲堆內存和棧內存。所指的棧就是如今所說的虛擬機棧,或者說是虛擬機棧中的局部變量表部分。數據結構
Java虛擬機規範中規定了兩種異常,若是線程請求的棧的深度大於了虛擬機規定的容許的棧的深度,將會拋出StackOverflowErro異常.當Java虛擬機動態擴展的時候沒法申請到足夠的內存時候,就會拋出OutOfMemoryErro異常。併發
本地方法棧和Java虛擬機棧很是的相似,Java虛擬機棧是爲虛擬機執行Java方法服務,本地方法棧是爲Java虛擬機運行本地方法服務,他們產生的異常種類也是相同的。app
虛擬機管理內存的最大的區域,Java堆是被全部線程共同管理的一塊內存區域,在虛擬機啓動的時候建立。這個內存區域的主要目的就是存放Java對象實例,幾乎全部的實例都是在堆中進行分配的。函數
從內存回收角度看,Java堆中還能夠分爲新生代和老年代。從Java內存分配角度看,線程共享的Java堆中可能劃分爲多個線程私有的分配緩衝區,不論如何劃分,都與存放的內容無關,不管哪一個區域,存儲的都是對象的實例,不管怎麼劃分都是爲了更好的分配內存和回收內存。佈局
方法區與Java堆同樣,是各個線程共享的內存區域,它用於存儲已經被虛擬機加載的類的信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。測試
在Hotspot虛擬機中,不少人把方法區當作是永久代,本質上不等價,這裏主要是Hotspot虛擬機設計團隊把GC分代收集擴展至方法區了,或者說使用永久代來實現方法區而已 ,這樣垃圾收集器就能夠像管理堆同樣來管理方法區了。
Java虛擬機規範對方法區的限制是很是寬鬆的,除了和Java堆同樣不須要連續的內存和能夠選擇固定的大小和可擴展之外,還能夠選擇不實現垃圾收集。垃圾收集行爲在這個區域是不多見的,這個區域的回收目標是對常量池的卸載和對類型的卸載,尤爲是對類型的卸載,條件至關苛刻。
用於存放編譯生成的各類字面量和符號引用,這部份內容將在類加載侯進入方法區的運行時常量池中存放。Java語言並不要求常量必定只有在編譯器才能產生,也就是並不是預製入class文件中的常量內容才能進入方法區運行時常量池,運行期間產生的常量也可能放入常量池中。
虛擬機每次遇到一個new指令的時候,首先會監測這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載,解析和初始化,若是沒有,那必須執行相應的類加載過程。
在類加載檢查經過之後,接下來虛擬機將爲新生對象分配內存,對象所須要的內存大小將在這個類加載完成之後就能夠徹底肯定,爲對象分配內存就至關於從Java堆中劃分一塊內存出來。
若是Java堆中的內存是絕對規整的,全部用過的內存都放在一邊,沒有用過的內存放在另一邊,中間存放着一個指針做爲分界點,當爲對象分配內存的時候,移動指正向空閒一邊移動和對象內存大小的相等的距離,這種分配方式就叫作指針碰撞。
若是Java堆中的內存不是規整的,已經使用的內存和空閒的內存相互交錯的話,就不能夠用指針碰撞方式分配內存,虛擬機必須就必須維護一個列表實例,記錄哪些內存可用,在分配的時候從列表中選取一個足夠大的空間分配給對象實例,而且更新記錄表,這種分配方式叫作空閒列表。
虛擬機採用哪一種方式分配內存的時候,取決於Java堆是否規整,Java堆是否規整又由所採用的垃圾收集器時候帶有壓縮整理功能決定。
除了考慮如何劃分空間以外,還學要考慮的問題是對象的建立在虛擬機中是不是很是頻繁的行爲,即便僅僅修改一個指正的行爲,在虛擬機進行併發的狀況下也不是線程安全的,可能出現正在給對象A分配內存的收,指針還沒來得及修改,對象B又同時修改了原來的指針來分配內存。解決這個問題有以下兩個方案:
一種是在對象分配內存的時候進行同步處理,另一種就是對象在分配內存的時候按照線程劃分在不一樣的空間中進行分配內存,即每一個線程在Java堆中預先奉陪一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB),哪一個線程須要分配內存就在哪一個線程的TLAB上進行分配,只有當TALB用完的時候分配新的TLAB的時候才須要同步鎖定。虛擬機是否採用TLAB,能夠經過**-XX:+/-UserTLAB**參數設定。
內存分配完成之後,虛擬機須要將分配的內存空間都初始化爲零值(不包括對象頭),若是使用TLAB,這一工做能夠提早至TLAB分配的時候進行,這一步驟操做保證了對象的實例字段在Java代碼中能夠不賦值初始值就能夠直接使用。
接下來Java虛擬機還要對對象就行必要的設置,好比這個對象是哪一個類的實例,如何才能找到類的原始數據星系,對象的hash碼,對象的GC分帶年齡等信息。這些信息都放在對象的對象頭中。
通過上面的步驟之後,從虛擬機的角度看,一個新的對象就已經產生了,可是從Java程序的角度看,對象建立纔剛剛開始,初始化還沒執行,全部的字段都仍是默認值,因此開始執行構造函數的初始化,從而按照程序員的思想進行初始化。
對象在內存中的存儲能夠分爲三塊區域,對象頭,實例數據,對其填充。
對象頭包括兩個部分信息:
第一部分用於存儲對象自身運行時候的數據,如哈希碼,GC分代年齡,鎖狀態標誌,線程持有的鎖,偏向線程ID,偏向時間戳等等。這部份數據的長度在32位和64位的系統中的長度分別是32和64位,官方稱爲「Mark Word」。對象須要存儲的運行時數據不少,已經超過了32位和64位的Bitmap結構所能存儲的值,可是對象頭信息是對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成了額一個非固定的數據結構,以便在極小的空間內存儲儘可能多的信息,它會更具對象的狀態複用本身的存儲空間。
第二部分就是類型指針,即對象指向他類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例,並非全部的虛擬機都必須在對象數據上保留類型執行,換句話說,查找對象的元數據並不必定要經過對象自己。另外,若是對象是個數組,那麼在對象頭中還必須有一塊用於記錄數組長度的指針,由於虛擬機能夠經過普通的Java對象的源數據信息肯定Java對象的大小,可是數據的元數據沒法肯定數據的大小。
實例數組部分是對象真正存儲的有效信息,也是在程序代碼中定義的各類類型字段的內容。不管是從父類繼承下來的,仍是在子類中定義的,都須要記錄起來,這部分的存儲順序會受到虛擬機分配策略參數和字段在Java源碼中定義順序的影響。Java虛擬機的分配策略中相同寬度的字段老是會分配到一塊兒,在知足這個條件的前提下,在父類中定義的變量會出如今子類以前,子類中較窄的變量可能會插入到父類的變量的空隙之中。
對齊填充部分並非必然存在的,也沒有特別的含義,它僅僅起着佔位符的做用,因爲Java中Hotspot VM的自動內存關係系統要求對象的起始地址必須是8字節的整數倍,換句話說,對象大小必須是8字節的整數倍,對象頭正好是8字節的整數倍,所以當對象的實例數據沒有對齊的時候,須要經過對齊填充來進行補全。
Java程序經過操做Java棧本地變量表中的reference數據來操做堆上的具體對象。 目前Java虛擬機對象訪問方式有使用句柄和直接指針兩種方式。
句柄訪問:Java堆中會劃分一塊內存做爲句柄池,存儲對象的的句柄地址,句柄中包含了對象的實例數據與類型數據各自具體的地址信息。本地變量表中reference直接指到句柄池,句柄池總的指向類型數據的指針指向方法區的類型數據,指向實例數據的指針指向實例池中的對象的實例數據。
指針訪問的方式:Java堆對象的內存佈局必須考慮如何防止類型數據相關的信息。 Java本地棧變量表中的reference直接指向Java堆中的對象實例數據和對象實例數據類型的指針,對象類型實例數據的指針指向方法區中對象類型數據。
使用句柄訪問的好處就是reference中存儲的是穩定的句柄地址,在對象唄必定(垃圾收集是移動對象是很是廣泛的行爲)最會改變句柄中規定實例數據的執行,而reference自己不須要修改。
使用指針訪問的最大的好處就是速度快,節省了一次指針定位的時間開銷,因爲對象的訪問在Java中很是的頻繁,一次這類的開銷聚沙成塔侯也是一項很是可觀的執行成本。
Java堆用於存儲對象實例,只要不斷的建立對象,而且保證GC Roots到對象之間有可達路徑來避免垃圾回收繼承消除這些對象,那麼對象數量到達對打堆的容量的時候就會產生內存溢出異常。經過-XX:+HeapDumpOnOutOfMemoryError可讓虛擬機在內存出現溢出異常是Dump出當前的內存對轉儲快照以便於時候進行分析。
package basic; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * @program: demo * @description: demo * @author: lee * @create: 2018-09-10 **/ public class Demo { public static void main(String[] args) { List list = new ArrayList(); while (true){ list.add(new Object()); } } }
輸出結果
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid15000.hprof ... Heap dump file created [2314303873 bytes in 10.848 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2245) at java.util.Arrays.copyOf(Arrays.java:2219) at java.util.ArrayList.grow(ArrayList.java:242) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) at java.util.ArrayList.add(ArrayList.java:440) at basic.Demo.main(Demo.java:18) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
因爲在Hotspot虛擬機中並不區分本地方法棧和虛擬機棧,所以棧容量只須要由-Xss參數設置就能夠。
以下代碼
/** * @program: demo * @description: demo * @author: lee * @create: 2018-09-10 **/ public class Demo { public static void main(String[] args) { test(); } public static void test(){ test(); } }
輸出結果
Exception in thread "main" java.lang.StackOverflowError at basic.Demo.test(Demo.java:19) at basic.Demo.test(Demo.java:19) at basic.Demo.test(Demo.java:19)
Java虛擬機棧包括了兩種異常:
若是線程請求的棧的深度大於了虛擬機容許的棧的深度的最大值,那麼將會出現StackOverflowError異常。
若是虛擬機擴展棧的時候若是申請到足夠的內存的話,則拋出OutOfMenmoryError異常。
在本地測試中,使用-Xss參數介紹棧內存容量,結果拋出StackOverflowError異常,異常出現時輸出堆棧的深度相應縮小。
定義了大量的本地變量,增大了此方法中本地變量表的長度,結果仍是拋出StackOverflowError異常。異常時輸出棧堆深度相應縮小。
在單線程狀況下,不管是棧幀太大仍是訊積極容量過小,當內存沒法分配的時候,虛擬機拋出的都是StackOverflowError異常。
運行時常量池是方法區的一部分,所以這兩個區域的測試只能放到一塊兒測試。
String.intern()方法是一個native方法,他的做用是:若是字符串常量池中已經包含了一個等於此String對象的字符串,則返回表明池中這個字符串的String對象;不然將這個string對象加入到常量池中,並返回此string對象的引用。
/** * @program: demo * @description: demo * @author: lee * @create: 2018-09-10 **/ public class Demo { public static void main(String[] args) { //使用list保持常量池的引用,避免Full GC回收常量池的行爲 List <String> list = new ArrayList<String>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } }
Java1.7之前會發生內存溢出異常,Java1.7之後就不會發生異常了。由於1.7之前intern若是字符串是首次出現,則intern方法會把字符串添加到常量池中,返回此常量池的應用,不然直接返回常量池的引用。
請繼續看
package basic; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * @program: demo * @description: demo * @author: lee * @create: 2018-09-10 **/ public class Demo { public static void main(String[] args) { //使用list保持常量池的引用,避免Full GC回收常量池的行爲 String str1 = new StringBuilder("計算機").append("軟件").toString(); System.out.println(str1.intern()==str1); String str2 = new StringBuilder("計算機").append("軟件").toString(); System.out.println(str2.intern()==str2); } }
輸出結果
true false
爲何會出現這樣的狀況呢,緣由是在Java1.6之前,intern方法會把首次遇到的字符串實例複製到永久代中而且返回永久代中的字符串實例引用,而建立對象又是在Java堆中建立的,因此必然不是同一個引用。在Java1.7之後,intern的實現不會再複製實例,只是在常量池中記錄首次出現的字符串的實例,所以intern 返回的是引用和由stringbuilder建立的那個字符串實例是同一個值。第二次的時候intern返回了第一個值的應用,stringbuilder又新生成了一個對象,因此對象實例是不一樣的。
方法區用於存放Class相關的信息,如類名,訪問修飾符,常量池,字段描述,方法描述等等。對於這個區域的測試基本思路就是產生大量的類去填充方法區,直到方法區溢出。
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M -vmargs 說明後面是VM的參數,因此後面的其實都是JVM的參數了
-Xms128m JVM初始分配的堆內存
-Xmx512m JVM最大容許分配的堆內存,按需分配
-XX:PermSize=64M JVM初始分配的非堆內存
-XX:MaxPermSize=128M JVM最大容許分配的非堆內存,按需分配