JVM_體系

JVM位置

JVM是運行在操作系統之上的,它與硬件沒有直接的交互。

在這裏插入圖片描述

JVM體系結構

在這裏插入圖片描述

類裝載器ClassLoader

負責加載class文件,class文件在文件開頭有特定的文件標示,將class文件字節碼內容加載到內存中,並將這些內容轉換成方法區中的運行時數據結構並且ClassLoader只負責class文件的加載,至於它是否可以運行,則由Execution Engine決定 。
在這裏插入圖片描述

種類

在這裏插入圖片描述

虛擬機自帶的加載器

  • 啓動類加載器(Bootstrap)C++
  • 擴展類加載器(Extension)Java
  • 應用程序類加載器(AppClassLoader)Java也叫系統類加載器,加載當前應用的classpath的所有類

用戶自定義的加載器

java.lang.ClassLoader的子類,用戶可以定製類的加載方式

加載時機(類僅加載一次)

  1. 實例化該類對象時
  2. 調用該類的靜態方法或靜態屬性時
  3. JVM啓動時調用main方法所在的類
  4. 調用java某些反射的方法(例如JDBC加載驅動類)
  5. 初始化該類的子類時

雙親委派機制

當一個類收到了類加載請求,首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應該傳送到啓動類加載其中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的Class),子類加載器纔會嘗試自己去加載。

採用雙親委派的一個好處是比如加載位於 rt.jar 包中的類 java.lang.Object,不管是哪個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個 Object對象。

執行引擎 Execution Engine

負責解釋命令,提交操作系統執行。

本地接口 Native Interface

本地接口的作用是融合不同的編程語言爲 Java 所用,它的初衷是融合 C/C++程序,Java 誕生的時候是 C/C++橫行的時候,要想立足,必須有調用 C/C++程序,於是就在內存中專門開闢了一塊區域處理標記爲native的代碼,它的具體做法是Native Method Stack中登記 native方法,在Execution Engine 執行時加載native libraies

目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用中已經比較少見。因爲現在的異構領域間的通信很發達,比如可以使用 Socket通信,也可以使用Web Service等等。

本地方法棧 Native Method Stack

它的具體做法是Native Method Stack中登記native方法,在Execution Engine 執行時加載本地方法庫。

程序計數器/PC寄存器 Program Counter Register

每個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,也即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不記。

這塊內存區域很小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。

如果執行的是一個Native方法,那這個計數器是空的。

用以完成分支、循環、跳轉、異常處理、線程恢復等基礎功能。不會發生內存溢出(OutOfMemory=OOM)錯誤

方法區 Method Area

供各線程共享的運行時內存區域。它存儲了每一個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容。上面講的是規範,在不同虛擬機裏實現是不一樣的,最典型的就是永久代(PermGen space)和元空間(Metaspace)。實例變量存在堆內存中,和方法區無關。

實際而言,方法區(Method Area)和堆一樣,是各個線程共享的內存區域,它用於存儲虛擬機加載的:類信息+普通常量+靜態常量+編譯器編譯後的代碼等等,雖然JVM規範將方法區描述爲堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。

它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 纔會釋放此區域所佔用的內存。

在Java8中,永久代已經被移除,被一個稱爲元空間的區域所取代。元空間的本質和永久代類似。

元空間與永久代之間最大的區別在於:
永久代使用的JVM的堆內存,但是java8以後的元空間並不在虛擬機中而是使用本機物理內存。

因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入native memory, 字符串池和類的靜態變量放入 java 堆中,這樣可以加載多少類的元數據就不再由MaxPermSize 控制, 而由系統的實際可用空間來控制。

棧 Stack

棧也叫棧內存,主管Java程序的運行,是在線程創建時創建,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來說不存在垃圾回收問題,只要線程一結束該棧就Over,生命週期和線程一致,是線程私有的。8種基本類型的變量+對象的引用變量+實例方法都是在函數的棧內存中分配

存儲數據類型

  • 本地變量(Local Variables):輸入參數和輸出參數以及方法內的變量
  • 棧操作(Operand Stack):記錄出棧、入棧的操作
  • 棧幀數據(Frame Data):包括類文件、方法等等

運行原理

棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀 F1,並被壓入到棧中,
A方法又調用了 B方法,於是產生棧幀 F2 也被壓入棧,
B方法又調用了 C方法,於是產生棧幀 F3 也被壓入棧,
……
執行完畢後,先彈出F3棧幀,再彈出F2棧幀,再彈出F1棧幀……

遵循“先進後出”/“後進先出”原則。

每個方法執行的同時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每一個方法從調用直至執行完畢的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。棧的大小和具體JVM的實現有關,通常在256K~756K之間,約等於1Mb左右
在這裏插入圖片描述

堆 Heap

內存模型

在這裏插入圖片描述

堆內存調優

在這裏插入圖片描述

public static void main(String[] args){
	//返回 Java 虛擬機試圖使用的最大內存量。
	long maxMemory = Runtime.getRuntime().maxMemory() ;
	//返回 Java 虛擬機中的內存總量。
	long totalMemory = Runtime.getRuntime().totalMemory() ;
	System.out.println("MAX_MEMORY = " + maxMemory + "(字節)、" + (maxMemory / (double)1024 / 1024) + "MB");
	System.out.println("TOTAL_MEMORY = " + totalMemory + "(字節)、" + (totalMemory / (double)1024 / 1024) + "MB");
}

在這裏插入圖片描述
VM參數: -Xms1024m -Xmx1024m -XX:+PrintGCDetails
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

類的生命週期

在這裏插入圖片描述

裝載

  • 通過一個類的全限定名來獲取定義此類的二進制字節流
  • 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
  • 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口

連接

驗證

  • 目的:確保Class文件的字節流中包含的信息符合當前虛擬機的要求,不會危害虛擬機自身的安全。
  • 步驟:
    • 文件格式驗證(字節流)
    • 元數據驗證(方法區)
    • 字節碼驗證(方法區)
    • 符號引用驗證(方法區)

準備

  • 爲靜態變量在方法區分配內存並設置靜態變量爲默認值
  • 爲靜態常量在方法區分配內存並賦值

解析

  • 將常量池內的符號引用替換爲直接引用的過程
    • 符號引用:包含類的信息,方法名,方法參數等信息的字符串,供實際使用時在該類的方法表中找到對應的方法
    • 直接引用:偏移量,通過偏移量可以直接在該類的內存區域中找到方法字節碼的起始位置。

初始化

  • 執行類構造器()方法的過程
  • 按照源文件中的順序收集類的靜態數據,併爲靜態變量賦初始值
  • 靜態語句塊只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問

對象實例化

  • new關鍵字爲對象在堆中分配合適的內存,併爲對象的成員屬性賦默認值
    • 對象優先在Eden中分配
    • 大對象直接進入老年代
    • 長期存活的對象進入老年代
    • 動態對象年齡判定
    • 空間分配擔保
  • 執行構造塊(優先)和構造器爲屬性賦初始值
    • 如果父類構造器中調用了非靜態方法,同時子類重寫了該方法,則創建子類對象時,子類構造器中調用super實例化父類對象時,父類構造器調用子類重寫後的方法,因爲非靜態方法前面有一個默認的對象this,構造器中this表示正在創建的對象,而此時正在創建子類對象,所以調用子類重寫後的方法。
  • 構造塊與非靜態成員屬性誰在前誰先執行

垃圾回收(Garbage Collection,GC)

判斷對象是否需要被回收

引用計數法(Reachability Counting)

  • 通過在對象頭中分配一個空間來保存該對象被引用的次數(Reference count)。如果該對象被其他對象引用,則它的引用計數+1,如果刪除對該對象的引用,那麼它的引用計數-1,當該對象的引用計數爲0,那麼該對象就會被回收。
  • 弊端:循環引用

可達性分析法(Reachability Analysis)

  • 通過一些被稱爲GC Roots的對象作爲起點,從這些節點開始向下搜索,搜索走過的路徑被稱爲Reference Chain,當一個對象到GC Roots沒有任何引用鏈相連時(即從GCRoots節點到該對象不可達),則證明該對象是不可用的。
GC Roots
  • 虛擬機棧(棧幀中的局部變量表)中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(一般說的Native方法)引用的對象

回收對象之前判斷是否有必要執行finalize方法

  • finalize方法在對象被垃圾回收之前執行且僅執行一次
  • 如果該對象執行過了finalize方法或者沒有重寫該方法,那麼認爲該對象沒有必要執行finalize方法,將其回收
  • 如歸該對象重寫了finalize方法並且沒有執行過,那麼會將其抽離到F-Queue隊列,由一個低優先級線程執行該隊列中對象的finalize方法

回收之後的內存處理

標記清理算法(Mark-Sweep)

  • 把內存去榆中可回收的對象標記出來,然後把這些垃圾清理掉,清理掉之後就出現了未使用的內存區域,等待再次被使用。
  • 優點:邏輯清晰,操作方法
  • 缺點:產生過多內存碎片

標記複製算法(Copying)

  • 將可用的內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當這一塊內存用完了,將還存活的對象複製到另一塊上面,然後再把已經使用過的內存空間一次性清理掉
  • 優點:保證率內存的連續可用,內存分配時不用考慮內存碎片等複雜情況,邏輯清晰,運行高效
  • 缺點:浪費很多堆空間

標記整理算法(Mark-Compact)

  • 標記過程與標記清理算法一樣,讓所有存活的對象都向一端移動,在清理掉端邊界以外的內存區域
  • 優點:解決了內存碎片、內存利用率低等問題
  • 缺點:內存頻繁變動,整理所有存活對象的引用地址,效率低

分代收集算法(Generational Collection)

  • 融合了上述三種基礎的算法思想,而產生的針對不同情況所採用的不同算法的一套組合

堆內存模型與回收策略

  • 新生代(1/3)
    • Eden(8/10)
      • 大多數情況下,對象會在新生代Eden區進行分配,當Eden區沒有足夠空間進行分配時,JVM會發起一次minor GC,minor GC相比major GC更頻繁,回收速度更快
      • minor GC之後,Eden清空,絕大部分對象被回收,存活的對象進入Survivor From區(若空間不夠,則直接接入Old)
      • 一般採用複製算法
    • Survivor
      • Survivor From(1/10)
      • Survivor To(1/10)
      • Eden與Old之間的緩衝區,分爲兩個區,每次執行minor GC,將Eden和其中一個區的存活對象複製到另一區(如果空間不夠,直接方法Old)
      • 存在意義:減少被送到老年代的對象,進而減少Major GC的發生,只有經歷過16次minor GC 還能在新生代存活的對象,纔會被送到Old
      • 分2區的意義:解決內存碎片化。每次Minor GC將之前Eden和From中存活的對象複製到To。下一次From與To職責對換,永遠有一個Survivor是空的,另一個非空是無碎片的。
    • 老年代(Old)
      • 由標記清除或者是標記清除與標記整理的混合實現

對象終結

卸載類型

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章