參考文章:html
http://gityuan.com/2016/01/09/java-memory/ (推薦牛人的博客)java
https://www.guru99.com/java-stack-heap.htmlgit
https://www.zhihu.com/question/21539353bootstrap
http://www.javashuo.com/article/p-wkgtrwav-gc.html多線程
深刻Java虛擬機函數
Java虛擬機在執行java程序的過程當中會把其所管理的內存區域劃分爲若干個不一樣的數據區域。 詳情以下圖所示:操作系統
程序計數器是一塊較小的空間,能夠當作當前線程所執行的字節碼的行號指示器,在虛擬機的概念模型中,字節碼解釋器工做經過改變程序計數器的值來選取下一條須要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能須要依賴這個計數器完成。.net
因爲虛擬機的多線程是經過線程輪流切換分配處理器執行時間的方式來實現的,因此任何一個時刻,一個處理器都會執行一條線程中的指令。所以,爲了切換線程能恢復到正確的執行位置,每條線程都須要一個獨立的程序計數器,各個線程之間互不影響,獨立存儲,是一個線程私有的內存。插件
若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是執行Natvie方法,這個計數器值則爲空。線程
這個內存區域是惟一一個在Java虛擬機規範中沒有規定任何Out Of Memory Erorr狀況的區域。
Java棧,即Java虛擬機棧是線程私有的,他的生命週期和線程相同。Java棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀用於存儲局部變量表,操做數棧,動態連接,方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程.
局部變量表:存放了編譯器可知的各類基本數據類型,對象引用類型(reference類型,不一樣於對象自己,他多是一個指向對象起始地址的引用指針,也多是指向一個表明對象句柄或者其餘於此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。
局部變量表的所需內存空間在編譯期間完成分配,當進入一個方法時候,這個方法所須要分配的局部變量空間是徹底肯定的,方法運行時不會改變局部變量表的大小。
Java棧是爲虛擬機執行Java方法(即字節碼)服務。
本地方法棧執行的是Native方法服務
Java堆是被全部線程共享的一塊內存區域。 堆的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。 Java堆也是垃圾收集器管理的主要區域,即"GC堆"。
Java堆能夠出於物理上不連續的內存空間中,邏輯上連續便可。
方法區與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。
存儲內容:類中的靜態常量、類中定義爲final類型的常量、類中的Field信息、類中的方法信息
運行時常量池 是方法區的一部分,Class文件中除了有類的版本,字段,方法,接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
運行時常量池相對於Class常量池來講,更具備動態性,Java語言不要求常量必定只有在編譯期才能產生,也就是並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入常量池,如String.intern()方法。
關於各個部分的聯繫,能夠查看下圖(雖然這張圖講的是GC Root的,可是我以爲對於理解JMM挺有用的):
這裏首先總結一下對象的建立過程:
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這一個過程爲類加載機制。
類從被加載到虛擬機內存開始,到卸載出內存爲止,他的整個生命週期包括以下階段:
加載,驗證,準備,初始化和卸載階段的順序是肯定的,而解析階段則不必定了,他在某些狀況下可在初始化完成後在開始,這是爲了支持Java語言的運行時綁定。
關於何時開始加載的過程,Java虛擬機規範中沒有進行強制約束,可由虛擬機自由把握。可是對於初始化階段,虛擬機嚴格規定了有且只有5種狀況必須當即對類進行"初始化"(加載,驗證,準備天然須要在此以前開始)。
java.lang.invoke.MethodHandle
實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokestatic的方法的句柄,而且這個方法句柄對應的類沒有初始化,則觸發初始化。加載
加載階段,虛擬機須要完成三件事情:
在加載階段可使用系統提供的引導類加載器來完成,也能夠由用戶自定的類加載器完成(如Android的插件化技術)。
詳細可參考IBM這篇文章
引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。
擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。能夠經過ClassLoader.getSystemClassLoader()來獲取它。
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區裏,方法區中的數據存儲格式由虛擬機自行定義。而後在內存中實例化一個java.lang.Class對象,這個對象做爲程序訪問方法區中這些類型數據的外部接口。
加載階段和鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始了,但這些夾在加載階段之中進行的動做,仍屬於鏈接階段的內容。
驗證
驗證是鏈接的第一步,目的是爲了確保Class文件字節流中包含的信息符合虛擬機要求。
驗證階段包含4個階段的檢驗動做:
(如驗證是否以魔數開頭,主次版本號是否在虛擬機處理範圍以內等等)
如這個類是否有父類?
這個類的父類是否繼承了不被容許繼承的類(final關鍵字)
...
符號引用中經過字符串描述的全限定名是否能找到對應的類。
再指定類中是否存在符合方法字段描述符以及簡單名稱所描述的方法和字段。
符號引用中的類,字段,方法的訪問性(private,protected,public,default)是否能夠被當前類訪問.。
準備
準備階段是正式將類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。
須要注意的是,這個時候進行內存分配得僅僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量會在對象實例化時候隨着對象一塊兒分配在Java堆中。其次,上述所說的設置類變量初始值是指的0值,假設類變量定義以下:
public static final num=3;
則num在準備階段初始化的值爲0而不是3,由於這個時候還沒有開始執行任何Java方法,而把num指定爲3的putstatic指令是在程序被編譯後,存放於類構造器<clinit>()方法中的,因此num=3的動做在初始化階段纔會執行。
But,若是上述代碼加了一個final字段,那就不同了:
public static final final num=3;
這時候Javac將會爲num生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue的設置將num賦值爲3。
public class Test { private static int num=3; private static final int finalnum=3; public static void main(String[] args) { } } //使用javap反編譯查看Test.class,省略無關部分 //javap -v -p Test.class private static int num; descriptor: I flags: ACC_PRIVATE, ACC_STATIC private static final int finalnum; descriptor: I flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL ConstantValue: int 3
這裏順便記錄一下<init>和<clinit>的區別,能夠看這個:
<init> is the (or one of the) constructor(s) for the instance, and non-static field initialization.
<clinit> are the static initialization blocks for the class, and static field initialization.
class X { static Log log = LogFactory.getLog(); // <clinit> private int x = 1; // <init> X(){ // <init> } static { // <clinit> } }
解析
解析階段是虛擬機將常量池內的符號引用替換成直接引用的過程,在上面的符號引用驗證中提到過。
符號引用替換成直接引用的過程有以下幾種狀況(詳細內容見深刻理解Java虛擬機P223):
上述階段經過了,則說明解析的工做就完成了。
初始化
初始化工做是類加載過程的最後一步,前面介紹的類加載過程當中,除了在加載階段用戶能夠經過自定義ClassLoader參與外,其他的皆有虛擬機主導完成。到初始化階段,才真正執行類定義中的Java代碼。
init是對象構造器方法,也就是說在程序執行 new 一個對象調用該對象類的 constructor 方法時纔會執行init方法
在準備階段,系統已經默認給類變量(被static修飾的變量)賦值過一次初始值(由系統決定)了,初始化階段就是執行程序猿賦值的過程。或者說初始化階段是執行<clinit>方法的過程。
clinit方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的。 編譯器收集的順序是由語句在源文件中出現的順序決定靜態語句塊只能訪問到定義在靜態語句塊以前>的變量,定義在他以後的變量,只能賦值,不能訪問[1]
clinit方法不須要顯示的調用父類構造器,虛擬機會保證在子類的clinit方法執行以前,父類的>clinit已經執行完畢。第一個執行clinit方法的確定是java.lang.Object。
因爲父類的clinit方法先執行,也就意味着父類中定義的靜態語句塊優於子類變量賦值操做。[2]
clinit方法對類或者接口來講不是必須的,若是一個類沒有靜態語句塊,也沒有對static變量賦值的操做,那麼編譯器能夠不爲這個類生成clinit方法。
接口中也會有clinit方法,惟一與類中執行不一樣的是,執行接口clinit方法不須要先執行父接口的>clinit方法,只有當父接口中的變量使用時候,父接口才會初始化。
虛擬機保證一個類的clinit方法只會執行一次。
[1]的驗證:
static { num=2323;//能夠賦值 System.out.print(num+"");//IDE會提示不能訪問 } private static int num=3; //----------------------------------------下面狀況經過 private static int num=3; static { num=2323; System.out.print(num+""); }
[2]的驗證:
public static void main(String[] args) { Child child = new Child(); System.out.print(child.K+""); } static class Parent{ public static int lll=23; static { lll=666; } } static class Child extends Parent{ public static int K=lll; } //輸出666
恰好整理一下static在代碼中的執行順序
若是類尚未被加載:
先執行父類的靜態代碼塊和靜態變量初始化,而且靜態代碼塊和靜態變量的執行順序只跟代碼中出現的順序有關。
執行子類的靜態代碼塊和靜態變量初始化。
執行父類的實例變量初始化
執行父類的構造函數
執行子類的實例變量初始化
執行子類的構造函數
若是類已經被加載:
.class文件是二進制字節流形式
隨着虛擬機的不斷髮展,不少程序語言開始選擇與操做系統和機器指令集無關的格式做爲編譯後的存儲格式(Class文件),從而實現」Write Once, Run Anywhere」。 Java設計之初,考慮後期能讓Java虛擬機運行其餘語言,目前有愈來愈多的其餘語言均可以直接須要在Java虛擬機,虛擬機只能識別Class文件,至因而由何種語言編譯而來的,虛擬機並不關心。
類卸載的過程及觸發條件
在類使用完以後,知足下面的情形,會被卸載:
該類在堆中的全部實例都已被回收,即在堆中不存在該類的實例對象。
加載該類的classLoader已經被回收。
該類對應的Class對象沒有任何地方能夠被引用,經過反射訪問不到該Class對象。
若是類知足卸載條件,JVM就在GC的時候,對類進行卸載,即在方法區清除類的信息。