JVM01----JVM結構
Java虛擬機器(Java virtual machine)實現了Java語言最終要得特徵:即平臺無關性。
平臺無關性原理:編譯後得Java程式(.class)檔案由JVM執行。JVM遮蔽了與具體平臺相關的資訊,使程式可以在多種平臺上不加修改的執行。JVM在執行位元組碼的時候,把位元組碼解釋成具體平臺上的機器指令執行。因此實現Java平臺無關性。
一. JVM結構圖
1. Java程式的執行流程
所有的Java程式程式碼必須儲存在*.java檔案中,這些稱為原始碼。這些原始碼並不能夠直接執行,必須通過javac.exe命令將它們編譯成*.class檔案,而後用java.exe命令在JVM程序中解釋此流程。
實際上當JVM將所需要的*.class檔案載入到JVM程序之中,那麼這個過程就需要一個類載入器(ClassLoader)。有類載入器的好處在於:可以隨意指定程式*.class檔案的所在路徑。
2. JVM
JVM = 類載入器 classloader+ 執行引擎 execution engine + 執行時資料區域 runtime data area
首先Java原始碼檔案被Java編譯器編譯為位元組碼檔案,然後JVM中的類載入器載入完畢之後,交由JVM執行引擎執行。在整個程式執行過程中,JVM中的執行時資料區(記憶體)會用來儲存程式執行期間需要用到的資料和相關資訊。
JVM會提供本地方法介面和本地方法庫供JAVA程式呼叫,前面這些都是程式執行的輔助手段,而最終程式的執行是在執行時資料區裡面。
因此,在Java中我們常常說到的記憶體管理就是針對這段空間(執行時資料區域)進行管理(如何分配和回收記憶體空間)。
(1)ClassLoader
ClassLoader把硬碟上的class檔案載入到JVM中的執行時資料區域,但是它不負責這個類檔案能否執行,這是執行引擎負責的。
(2)執行引擎
作用就是來執行位元組碼,或者執行本地方法
(3)執行時資料區
JVM在執行期間,在執行時資料區對JVM記憶體空間的劃分和分配,劃分為了一以下5個區域來處理:
1)方法區(共享區)----1.7之後放在了堆中
它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯期編譯後的程式碼等資料;由於使用反射機制的原因,虛擬機器很難推測哪個類資訊不再使用,因此這塊區域的回收很難!
同樣當方法區無法滿足記憶體需求時,會丟擲OOM錯誤。
2)堆記憶體(共享區)
儲存所有引用資料型別的真實資訊。
對於大多數應用來說,Java堆是Jvm所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在jvm啟動時候建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。
如果在堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError異常。
3)棧記憶體(私有的)
(也就是虛擬機器棧,或者說是虛擬機器棧中區域性變量表部分):區域性變量表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference型別,它不等同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制代碼或其它與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址)。
- JVM棧是私有的,並且生命週期與執行緒相同。當執行緒執行完畢後,相應記憶體也就會被自動回收。
- 棧裡面存放的元素叫棧幀,每個方法從呼叫到執行結束,其實是對應一個棧幀的入棧和出棧。
- 這個區域可能有兩種異常:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常(如:將一個函式反覆遞迴自己,最終會出現這種異常)。如果JVM棧可以動態擴充套件(大部分JVM是可以的),當擴充套件時無法申請到足夠記憶體則丟擲OutOfMemoryError異常。
4)程式計數器(私有的)
是一個非常小的記憶體空間,小到可以忽略。主要是用來指向下一個將要執行的指令程式碼。如該執行緒正在執行一個Java方法,則計數器記錄的是正在執行的虛擬機器位元組碼地址,如執行native方法,則計數器值為空。比如下面“4”的提示正是它的一個表現;
1 public class Test 2 public static void main(String args[]){ 3 String str=null; 4 str.length(); 5 } 6 }
執行後,會在第4行的時候丟擲空指標異常:
Exception in thread "main" java.lang.NullPointerException at Test.main(Test.java:4)
為了執行緒切換後能夠恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各個執行緒之間計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。
5)本地方法棧(私有的)
每一次執行遞迴的方法處理的時候,實際上都會將上一個方法入棧;
本地方法棧VS 虛擬機器棧(棧記憶體):前者是為JVM使用到的Native方法服務,而後者是為JVM執行Java方法(也就是位元組碼)服務。本地方法棧也會丟擲StackOverflowError和OutofMemoryError異常。
比如:下面就會出現棧溢位的錯誤:
public class Test { public static void main(String args[]) { f(); } public static void f() { f(); } }
Exception in thread "main" java.lang.StackOverflowError at mytest2.Test.f(Test.java:8) at mytest2.Test.f(Test.java:8) at mytest2.Test.f(Test.java:8) at mytest2.Test.f(Test.java:8) at mytest2.Test.f(Test.java:8) at mytest2.Test.f(Test.java:8) at mytest2.Test.f(Test.java:8) at mytest2.Test.f(Test.java:8)
方法入棧:如下圖:假設方法A呼叫了B,方法B呼叫了C,方法C呼叫了D;首先A會入棧,然後B如棧,然後C,然後D。倘若一致程式在呼叫,導致棧一致在被佔用,就會導致程式不能夠往下繼續執行了。
Java記憶體管理就是指的執行時資料區,Java只能夠管理這部分。因此Java中的記憶體調優,實際上就是調的執行時資料區。
6)執行時常量池
- 存放類中固定的常量資訊、方法引用資訊等,其空間從方法區域(JDK1.7後為堆空間)中分配。
- Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有就是常量表,用於存放編譯期已可知的常量,這部分內容將在類載入後進入方法區(永久代)存放。但是Java語言並不要求常量一定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,執行期間也可將新內容放入常量池(最典型的String.intern()方法)。
- 當常量池無法在申請到記憶體時會丟擲OutOfMemoryError異常,上面也分析過了。
3. 堆和棧的區別
這是一個非常常見的面試題,主要從以下幾個方面來回答。
(1)各司其職
最主要的區別就是棧記憶體用來儲存區域性變數和方法呼叫資訊。
而堆記憶體用來儲存Java中的物件。無論是成員變數、區域性變數還是類變數,它們指向的物件都儲存在堆記憶體中。
(2)空間大小
棧的記憶體要遠遠小於堆記憶體,如果你使用遞迴的話,那麼你的棧很快就會充滿併產生StackOverFlowError。
(3)獨有還是共享
棧記憶體歸屬於執行緒的私有記憶體,每個執行緒都會有一個棧記憶體,其儲存的變數只能在其所屬執行緒中可見。
而堆記憶體中的物件對所有執行緒可見,可以被所有執行緒訪問。
(4)異常錯誤
如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常。
如果JVM棧可以動態擴充套件(大部分JVM是可以的),當擴充套件時無法申請到足夠記憶體則丟擲OutOfMemoryError異常。
而堆記憶體沒有可用的空間儲存生成的物件,JVM會丟擲java.lang.OutOfMemoryError。
二. JVM的分類
實際上,有三種JVM:
(1) SUN公司最早改良的HOTSPOT
(2) BEA公司的:JRockit
(3) IBM:JVM’s
而Oracle在收購了SUN和BEA公司後,得到了業內的兩個虛擬機器的版本。它正在試圖將這兩個虛擬機器合併成一個虛擬機器,從1.8開始,這個合併已經成功了,這是一個重要的JDK版本。
範例:取得當前的JVM版本(java -version)
所謂的混合模式(mixed mode):指的就是適合於編譯和執行。
範例:使用純解釋模式啟動:
範例:使用純編譯模式
HOTSPOT這個虛擬機器我們是可以進行控制的,但是沒有必要。實際上現在的JDK的設計都已經開始為伺服器而準備的。對於JVM的啟動模式分為兩種:
(1)-server:伺服器模式,佔用的記憶體大,啟動速度慢
(2)-client:本地單機執行程式模式,啟動速度快
比如:開啟JRE資料夾(安裝路徑),找到jvm.cfg,這個檔案中有如下內容:
說明clent忽略了。說明JAVA預設啟動方式是伺服器模式了。
三. Java物件訪問模式
Java的引用型別是最為重要的資料處理模型。而整個的引用資料型別處理之中會牽扯到:堆記憶體、棧記憶體、方法區。
下面以一個最簡單的程式程式碼為主:“Object obj=new Object()”,例項化了一個Object類物件。
(1) Object obj:描述的是儲存在棧記憶體之中,而儲存有堆記憶體的引用,這個資料會儲存在本地變量表中。(這個表中有變數名,棧,堆)
(2) New Object():一個真正的物件,物件儲存在堆記憶體中。
直觀思路整個引用的操作:
- 新定義的物件的名稱儲存在本地變量表,而後在這塊區域需要確認要與之對應的棧記憶體
- 通過變量表中的棧記憶體地址可以找到堆記憶體。
- 而後利用對記憶體的物件進行本地方法的呼叫(方法區)
對於引用資料型別的訪問實際上是存在兩種模式的:
1. 第一種:通過控制代碼訪問
控制代碼:可以理解為拿著物件的關鍵因素的東西,我們需要通過這個關鍵因素去尋找物件。Java堆中將會劃分出一塊記憶體來作為控制代碼池,refenerce中儲存的就是物件的控制代碼的地址,而控制代碼中包 含了物件例項資料與型別資料各自的具體地址資訊。
優點 : 最大的好處就是reference中儲存的是穩定的控制代碼的地址,在物件被移動(垃圾回收時移動物件是很常見的行為)時只會改變控制代碼中的例項資料的地址,而reference本身不需要修改。
使用:物件不是一次性找到的。需要通過控制代碼先找到物件例項資料,然後需要通過控制代碼找到物件型別資料。整個過程很麻煩。確保物件的真實存在。(也就是說,先要通過控制代碼找到物件在哪,然後通過控制代碼找到型別資訊,最後找到操作符方法)
2. 第二種:通過直接指標訪問(物件的儲存模式)
定義 : reference中儲存直接物件的地址,但是必須考慮放置訪問型別資料的相關資訊
優點 : 訪問速度快,節省了一次指標定位的時間開銷,省略了控制代碼到物件的查詢。
參考文獻:
https://blog.csdn.net/SEU_Calvin/article/details/51404589
《深入理解JAVA虛擬機器》
李興華老師的《Java記憶體模型》