Java 的 .class
文件經過類加載器加載進虛擬機內存裏面,由 JVM 虛擬機經過解析執行、或編譯執行。JVM 爲了方便管理被加載進來的 .class
內容,提出了 Java 虛擬機運行時數據區的概念。Java 虛擬機運行時數據區能夠劃分爲線程私有、線程共享兩大類型的數據區,其中線程私有包括程序計數器、虛擬機棧、本地方法棧;而線程共享包括 Java 堆、方法區。程序員
在沒有深刻理解 JVM 以前,咱們經常會把 Java 運行時數據區粗粒度地劃分爲 "堆"、"棧" 兩大部分,"堆" 是用來存放對象地實例,而 "棧" 則是用來存放對象地引用。隨着咱們對 JVM 地深刻學習,咱們發現 JVM 對內存的劃分遠比咱們在學習 Java 初級階段所認知的運行時數據區要複雜。C/C++ 的程序員須要手動釋放程序裏面不須要再用到的內存空間,而在 Java 裏面,虛擬機會自動地幫咱們回收不須要用到的資源,這就須要咱們深刻理解 JVM 運行時的內存劃分,有利於咱們對程序有更加深入的認識。算法
程序計數器(Program Counter Register)是一塊比較小的數據區域。由於在 Java 中是支持多線程的,那就意味着每條線程內須要一塊內存空間,記錄當前線程切換的時候(現場銷燬),當前字節碼執行的行號數,以便在該線程從新獲取到 CPU 執行時間的時候,能夠接着上次執行的字節碼行數號繼續執行(現場恢復)。而程序計數器則是爲了記錄當前線程的字節碼執行的行數號而提出的。數組
字節碼的解析器是根據該計數器的值來選取下一條須要解析執行的字節碼指令。Java 代碼的循環、跳轉、異常處理、線程恢復等都須要依靠該計數器來完成。數據結構
若是線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是 native 方法,這個計數器的值則爲空(Undefined)。程序計數器是 JVM 惟一一個沒有規定任何 OOM 狀況的區域。多線程
虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是 Java 方法執行的內存模型:在 Java 中每執行一個方法都會建立一個棧幀,而後系統會把這個棧幀壓入虛擬機棧。棧幀還能夠劃分爲局部變量表、操做數棧、動態連接、方法出口(方法返回地址)、以及額外的附加信息。Java 方法的執行就對應着棧幀在虛擬機中入棧和出棧的過程。佈局
局部變量表主要存放的是方法在執行過程當中的局部變量。如在方法中定義的八種基本類型、局部的變量等。學習
操做數棧存放的是操做數。操做數棧是一個棧結構的數據結構,棧元素能夠爲任意的 Java 數據類型。一個方法剛開始分配棧幀空間執行的時候,操做數棧是空的,當在執行方法內局部變量的運算的時候,操做數棧便會執行進棧/出棧操做。線程
動態連接的做用主要是支持 Java 語言的多態性(須要類加載、運行時才能肯定的方法)、動態性。指針
方法返回地址返回的是方法結果的返回。若是是正常返回,則調用程序計數器中的地址做爲返回;若是是異常返回,則是經過異常處理器表(非棧幀中的)來肯定。code
同時,Java 虛擬機規範中定義了虛擬機棧有兩種異常:
異常 | 定義 |
---|---|
StackOverFlowError | 當虛擬機請求的棧深度超過當前棧幀所一開始定義的棧幀深度時拋出 |
OutOfMemoryError | 若是虛擬機能夠在運行時動態申請棧內存空間,當 Java 虛擬機沒法申請到更多的棧內存的時候拋出 |
本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用時比較類似的,只不過虛擬機棧爲 Java 的普通方法提供內存空間,而本地方法棧則爲 Java 的本地方法提供內存空間。所以,有的虛擬機如 Sun HotSpot 虛擬機就直接地把虛擬機棧和本地方法棧合二爲一。
同時,本地方法棧和虛擬機棧同樣,也會拋出 StackOverFlowError
、OutOfMemoryError
兩種異常。
Java 堆是虛擬機中最大地一塊內存空間,同時該內存區域也是每條線程能夠共享的。Java 堆是在虛擬機建立的時候建立的,其主要的目的使用來存放對象實例、數組數據。但隨着 JIT編譯器的發展、逃逸分析技術的發展,使得對象能夠在棧中分配。
Java 堆是 GC 垃圾回收的主要區域。從內存回收的角度,由於 Java 堆主要採起分代收集算法,於是 Java 堆能夠劃分爲新生代、老年代,而新生代又能夠劃分爲 Eden空間、From Survivor和 To Survice空間。
從內存共享的角度,Java 堆能夠被劃分爲多個線程共享的分配緩衝區(Thread Local Allocation Buffer,TLAB)。
根據虛擬機規範,Java 堆能夠是物理內存上不連續,而在邏輯上連續的。同時,堆在沒法完成實力的分配,而且虛擬機沒法申請到更多的堆內存的時候,會拋出 OutOfMemoryError
異常。
在虛擬機中,對象實例主要是分配 Java 堆中,對象實例在內存的佈局以下:
對象實例的內存佈局分爲 3 塊區域:對象頭(Header)、實例數據(Instance)、對齊填充(Padding)。
對象頭(Header)在 HotSpot 中包括兩部分信息:用於存儲對象自身的運行時數據、類型指針。對於第一部分用於存儲自身運行時數據如上圖;而對於第二部分類型指針,即對象指向它的元數據的指針,虛擬機經過這個指針來肯定這個對象它屬於哪個類的實例。但並非全部的虛擬機都須要在對象頭數據上面保留類型指針,由於查找對象的元數據不必定須要經過對象的自己(如反射)。
實例數據部分是對象真正存儲的有效信息的,如在 Java 源碼中所定義的各類類型的字段內容,包括從父類繼承的數據內容。同時在實例數據部分的存儲收到虛擬機的分配策略(FieldsAllocationStyle)和字段在 Java 源碼中順序的影響。
對齊填充並非必然存在對象實例佈局中的,其沒有特別的含義。由於虛擬機的自動內存管理系統要求對象的大小必須須要 8 字節的整數倍。對象頭大小恰好是 8 字節的倍數(1 倍或 2 倍),當實例數據沒有對齊的時,就須要對齊填充來補全。
在 Java 堆中存放對象的實例的目的是爲了訪問使用對象,Java 程序須要經過虛擬機棧上本地變量表上的 reference 數據來操做堆上的具體對象。目前主流的訪問方式有使用句柄和直接指針兩種。
同時,該 Java 堆能夠拋出 OutOfMemoryError
異常。
方法區(Method Area)與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯的代碼等數據,GC 在該區域出現的比較少。同時,該方法區還包含着運行時常量池。
運行時常量池用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
各類字面量與 Java 語言層面概念相近,包含文本字符串、聲明爲 final 的常量等;
符號引用包括:類和接口的全限定名稱、字段的名稱和描述符、方法的名稱和描述符;
同時,方法區也能夠拋出 OutOfMemoryError
異常。
以上,包含了 JVM 運行時爲 Java 的 .class
加載進 JVM 所劃分的區域,分別爲線程私有和線程共享的區域。固然,這只是深刻理解 JVM 的過程的一小步,接下來還須要瞭解 JVM 對 Java 堆的垃圾標記、以及垃圾收集......