Java程序是如何運行的?
我們編寫的是.java文件,需要通過javac編譯生成.class文件,這樣class文件才能被JVM識別。 我們經??吹降?jar文件實際上是.class文件的壓縮包(減少文件數量,方便操作),加載到JVM后才能運行。
來自網絡
從上面可以看出,JVM由以下幾個部分組成
- 類加載、執行引擎、運行數據區以及垃圾回收器
。
運行數據區
圖片.png
運行時數據區主要可以分為5個區域
1 方法區(Area)
方法區由兩部分組成
- 永久代(Permanent Generation)
存放類結構信息的地方,包括常量池、靜態變量、構造函數、運行時常量池(Pool)等,以及編譯后的代碼(JIT)的代碼緩存。 雖然JVM規范將方法區描述為堆的邏輯部分,但它有一個別名non-heap(非堆),所以不要混淆它。
(永久代) - -XX:設置上限,-XX:設置最小值示例:VM Args:-XX:=10M -XX:=10M。 如果空間不足,就會引發 java.lang.: space 異常。
從JAVA8開始,持久代被完全刪除,并被另一個內存區域(也稱為元空間)取代。 它是本地堆內存的一部分。 可以通過-XX:和-XX:來調整。 當達到XX:指定的閾值時,就會開始清理該區域。 如果本地空間中的內存耗盡,則會收到 java .lang.: for space 的錯誤消息。
代碼緩存 - 該緩存區域用于存儲編譯后的代碼。 編譯后的代碼是本機代碼(與硬件相關),由JIT(Just In Time)編譯器生成,這是JVM特有的。
2 java堆(Heap)
java實例或對象存儲的地方。 這是GC的主要區域。 堆的大小可以通過 JVM 選項 -Xms 和 -Xmx 進行調整。 當堆耗盡時,JVM會拋出java.lang。 例外。 從存儲的內容我們很容易知道方法區和堆是所有java線程共享的。
堆分為:
3 java棧(Stack)
java 堆棧始終與線程相關聯。 每當創建一個線程時,JVM就會為該線程創建一個對應的java棧。 在這個java堆棧中,將包含多個堆棧幀。 每次運行方法時,都會創建一個堆棧幀來存儲局部變量表、操作堆棧和方法返回值。 每個方法從調用到執行完成的過程對應著一個棧幀被壓入java棧到出棧的過程。 所以java棧是線程私有的。
4 程序計數器(PC)
用于保存當前線程執行的內存地址。 由于JVM程序是由多個線程執行的(線程輪流切換),為了保證線程切換回來后能夠恢復到原來的狀態,需要有一個獨立的計數器來記錄之前被中斷的地方。 可見程序計數器也是線程私有的。
5 原生方法棧(Stack)
它與java棧的功能類似,但它服務于JVM使用的方法。 原生方法棧的參數順序和返回值與典型的C程序相同。
6 原生方法接口
主要調用C或C++實現的本地方法并返回結果。
7 直接存儲器
直接內存不是虛擬機運行時數據區域的一部分。
在NIO中,引入了一種基于通道和緩沖區的I/O方法,可以使用函數直接分配堆外內存,然后通過java堆中存儲的一個對象作為對這塊內存的引用進行操作。 -XX:設置最大值,默認與java堆的最大值相同。
類加載
JVM將類加載分為3步:
鏈接(link)分為3步,如下圖:
圖片.png
負載(負載)
查找并加載類二進制數據(查找并導入Class文件)
加載是類加載過程的第一個階段。 在加載階段,虛擬機需要完成以下三件事:
1) 通過類的全限定名獲取類定義的二進制字節流。
2)將此字節流表示的靜態存儲結構轉換為方法區的運行時數據結構。
3)在Java堆中生成一個代表該類的java.lang.Class對象,作為方法區數據的訪問入口。
與類加載的其他階段相比,加載階段(準確地說是加載階段獲取類的二進制字節流的動作)是最可控的階段,因為開發者可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
加載階段完成后,虛擬機外部的二進制字節流按照虛擬機要求的格式存儲在方法區中,同時在Java堆中也創建了一個類java.lang.Class的對象,所以Java 程序員可以提供訪問方法區中數據結構的接口。
那么class文件加載的原理是什么呢? 需要滿足雙親委派原則,通過類加載器來加載。 類加載器分為以下幾種類型:
[圖片上傳失敗...(image--22)]
在加載過程中,會首先檢查類是否已經加載。 檢查的順序是從下到上、從上到下逐層檢查。 只要加載了某個類,就認為已加載,并且保證所有此類類都被加載一次。 加載的順序是自上而下的,即上層嘗試逐層加載該類。
鏈接(分3步) 1)驗證:確保加載的類的正確性
驗證是連接階段的第一步。 該階段的目的是保證Class文件字節流中包含的信息滿足當前虛擬機的要求,不會危及虛擬機本身的安全。 在驗證階段,大致會完成四個階段的檢查動作:
文件格式驗證:驗證字節流是否符合Class文件格式的規范; 例如:是否以 開頭,主次版本號是否在當前虛擬機的處理范圍內,常量池中的常量是否有不支持的類型。
元數據驗證:對字節碼描述的信息進行語義分析(注:對比javac編譯階段的語義分析),確保其描述的信息符合Java語言規范的要求; 例如:這個類是否在.lang之外有除java之外的父類。
字節碼驗證:通過數據流和控制流分析,確定程序語義合法、邏輯正確。
符號引用驗證:確保解析動作能夠正確執行。
驗證階段非常重要,但不是必需的。 它對程序運行時間沒有影響。 如果引用的類已經被反復驗證,可以考慮使用--參數關閉大部分類驗證措施,以縮短虛擬機類加載時間。 時間。
2)準備工作:為類的靜態變量分配內存并初始化為默認值
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存將在方法區中分配。 此階段需要記住以下幾點:
假設一個類變量定義為:int value = 3; 那么準備階段之后變量值的初始值為0,而不是3,因為還沒有執行過任何Java方法,而給3賦值的指令是在程序編譯后存儲在類構造函數中的()方法,所以給3賦值的動作會在初始化階段執行。
3)解析:將類中的符號引用轉換為直接引用
解析階段是虛擬機用直接引用替換常量池中的符號引用的過程。 解析動作主要針對7類符號引用:類或接口、字段、類方法、接口方法、方法類型、方法句柄、調用限定符。 符號引用是描述目標的一組符號,可以是任何文字。
直接引用是直接指向目標的指針、相對偏移量或間接定位目標的句柄。
初始化
對類的靜態變量和靜態代碼塊進行初始化操作
初始化為類的靜態變量賦予正確的初始值,JVM負責類的初始化,主要針對類變量。 Java中初始化類變量有兩種方法: