1. 程式人生 > >JVM體系結構(類載入機制、記憶體結構、垃圾回收)

JVM體系結構(類載入機制、記憶體結構、垃圾回收)

        在上篇文章中我們稍微提了一下JVM,那在這篇文章中我們就來詳細聊聊。在這裡我推薦大家去閱讀《Java Web技術內幕》這本書籍,這本書的內容還是不錯的,有框架部分也有較為底層的部分,尤其是Java I/O、JVM以及中文編碼這幾大模組講的很詳細,大家有空可以去看一看。本篇文章也多處借用了書籍的內容。下面我們開始吧。

         JVM的全稱是Java Virtual Machine (Java虛擬機器),它通過模擬一個計算機來達到計算機所具有的計算功能。我們先來看看一個真實的計算機如何才能具備計算的功能。

以計算為中心來看計算機的體系結構可以分為如下幾個部分:

1、指令集,這個計算機所能識別的機器語言的命令集合。 2、計算單元,即能夠識別並且控制指令執行的功能模組。 3、定址方式,地址的位數、最小地址和最大地址範圍,以及地址的執行規則。 4、暫存器定義,包括運算元暫存器、變址暫存器、控制暫存器等的定義、數量和使用方式。 5、儲存單元,能夠儲存運算元和儲存操作結構的單元,如核心級快取、記憶體和磁碟等。

接下來我們再來看看JVM的體系結構:

1、位元組碼指令集,能被JVM解析執行。 2、類載入器,在JVM啟動時或者在類執行時將需要的class載入到JVM中。 3、執行引擎,執行引擎的任務是負責執行class檔案中包含的位元組碼指令,相當於實際機器上的CPU。 4、記憶體區,將記憶體劃分成若干個區以模擬實際機器上的儲存、記錄和排程功能模組,如實際機器上的各種功能的暫存器或者PC指標的記錄器等。 5、本地方法呼叫,呼叫C或C++實現的本地方法的程式碼返回結果。

 那麼,JVM和實體機到底有何不同呢?大體有如下幾點:

1、抽象規範,這個規範就約束了 JVM到底是什麼,它有哪些組成部分,這些抽象的規範都在 The Java Virtiual Machine Specification(java虛擬機器規範) 中詳細描述了。 2、具體的實現,所謂具體的實現就是不同的廠商按照這個抽象的規範用軟體或者軟體和硬體結合的方式在相同或者不同的平臺上的具體的實現。 3、執行中的例項,每個執行中的Java程式都是一個JVM例項。

接下來我們對JVM體系中的重點部分進行詳細講解。

JVM記憶體區域

PC暫存器:是一塊較小的記憶體空間,它可以看做是當前執行緒所執行的位元組碼的行號指示器,屬於獨佔區域。由於Java程式是多執行緒執行的,所以當多個執行緒交叉執行時,被中斷執行緒的程式當前執行到那條的記憶體地址必然要儲存下來,以便於它被恢復時再按照被中斷時的指令地址繼續執行下去。如果執行緒執行的是java方法,那麼,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址,如果是native方法,那麼值為undefined,這個區域也是唯一一個沒有規定記憶體溢位情況的區域。

java棧:描述的是java方法執行的動態記憶體模型,每一個方法執行都會建立一個棧幀,用於儲存區域性變量表、運算元棧、方法的返回地址,指向當前方法所屬的類的執行時常量池的引用。一個方法的開始和結束對應入棧和出棧。

區域性變量表:區域性變量表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。存放編譯期可知的各種基本資料型別,引用型別,returnAddress型別,區域性變量表的記憶體在編譯期就完成了分配,執行時大小不會改變,所以,當一個方法進入時,這個方法需要在幀中分配多少記憶體是固定的。若方法太多大於棧深度的話,就會報棧溢位stackOverflowError。當我們給棧加大深度(記憶體)的話,就會不停的一直建立,直到佔用記憶體大於虛擬機器設定的記憶體或者機器本身的記憶體,那麼就申請不到記憶體,就會報記憶體溢位outofmemoryError

本地方法棧:為虛擬機器執行native方法服務

堆:存放物件例項、垃圾收集器管理的主要區域、新生代,老年代。java堆中一些設定都是為了垃圾回收器。當然也會丟擲記憶體溢位outofmemoryError

方法區(可以理解為class檔案在記憶體存放的位置):儲存虛擬機器載入的類資訊(類的版本、欄位、方法、介面),常量、靜態變數、即時編譯器編譯後的的程式碼等資料。

方法區存放內容:1.類的全限定名(類的全路徑名)。2.類的直接超類的全限定名(如果這個類是Object,則它沒有超類)。3.類的型別(類或介面)。4.類的訪問修飾符,public,abstract,final等。5.類的直接介面全限定名的有序列表。6.常量池(欄位,方法資訊,靜態變數,型別引用(class))等 hostspot使用永久代來實現方法區(方法區標準 -- 永久代實現),好處是hostspot可以像管理java堆一樣管理這部分記憶體,可以省去專門為方法區編寫管理記憶體的程式碼的工作,當然僅限於hostspot,其他虛擬機器沒有永久代的概念。垃圾回收在方法區只有少部分的操作,因為成本比較高,而垃圾回收器的的效率比較低,部分回收操作如:常量池、物件型別的解除安裝。當申請記憶體失敗時同樣也會丟擲記憶體溢位 常量池:常量池資料編譯期被確定,是Class檔案中的一部分。儲存了類、方法、介面等中的常量,當然也包括字串常量。當載入一個方法時會為這個方法建立一個棧幀,而一些區域性變數和引用都會放在區域性變量表中,如果引用的是字串則不會放在堆中,而會放在方法區的常量池中,而池中的有字串表,它的資料結構為hashset來存放例項化的字串物件,那麼,引用相同的字串則會相等。如果我們是手動new的則一定在堆中。 字串池/字串常量池:是常量池中的一部分,儲存編譯期類中的字產生符串型別資料。

JDK1.7中JVM把字串常量池從方法區中移除了;JDK1.8中JVM把字串常量池移入了堆中,同時取消了“永久代”,改用元空間代替

垃圾回收

1、如何判定物件為垃圾物件(標記演算法)

引用計數法         在物件中新增一個引用計數器,當有地方引用它時,計數器加一,引用失效時,計數器為0,但無法處理迴圈引用的問題。(-verbose:gc和-xx:PrintGCDetail列印垃圾回收器的回收日誌  兩個分別為簡單和詳細)

可達性分析法        通過一系列的被稱為 GC root的物件作為起點,依次往下尋找和GC root有關或間接有關的一些物件,這就會形成一條引用鏈,這條引用鏈上的物件都是可用的。當一個物件到 GC root 沒有任何引用 鏈相連,則證明此物件是不可用。可被作為GC root物件的有:虛擬機器棧(區域性變量表)引用的物件、方法區中的類屬性引用的物件、方法區中的常量引用的物件,本地方法棧中引用的物件 

引用 強引用:引用只要還在,就不會被GC回收; 軟引用:在記憶體不足發生OOM之期,就回收掉該引用; 弱引用:在下一次GC前,就回收掉該引用; 虛引用:在任何時候可能被回收;

finalize若物件在進行可達性分析後發現沒有與 GC roots 相連線的引用鏈,那麼他將會被第一次標 記並進行一次篩選,篩選的條件是該物件是否有必要執行finalize()方法,當物件沒有重寫 finalize()方法或者 finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為沒必要執 行。 若該物件被判定為有必要執行 finalize 方法,則這個物件會被放在一個 F-Queue 佇列, finalize 方法是物件逃脫死亡命運的最後一次機會,稍後 GC 將對 F-queue 中的物件進行第二 次小規模的標記,若物件要在 finalize 中成功拯救自己—只要重新與引用鏈上的任何一個對 象建立關聯即可,那麼在第二次標記時他們將會被移出“即 將回收”集合。

2、如何回收

回收策略

1)、標記-清除演算法:經過可達性分析法後,對需要清除的物件進行標記然後清除。缺點,產生大量不連續的記憶體碎片、標記和清除效率都不高 (效率問題和空間問題)

2)、停止-複製演算法:在堆中分為兩塊區域新生代和老年代,老年代並不是特別關注,主要是新生代。新生代(Young Gen)又可以劃分為Eden(只要是新建立的物件都會被扔到這裡,也是垃圾回收器最常光顧),Survivor存活區(當 Eden 區中沒有足夠的 記憶體空間進行分配時,虛擬機器將發    起一次minorGC{minor gc:發生在新生代的垃圾收集動作,非常頻繁,一般回收速度比較快 full gc:發生在老年代的gc},若物件在 eden 出生並經過第一次 minor gc 後仍然存活,並且能被 survivor 容納的話,將被移到 survivor 空間中,並且物件年齡設為 1.)。老年代(TenuredGen):物件在 survivor 中每熬過一次 minor gc,年齡就+1,當他年齡達到一定程度(預設為 15), 就會晉升到老年代,垃圾回收也相對沒有那麼頻繁。

複製演算法就是將可用記憶體按照容量劃分為大小相等的兩塊,每次只使用其中一塊。 當這一塊的記憶體用完了,則就將還存活的物件複製到另一塊上面,然後再把已經使用過的內 存空間一次清理掉,那麼下一次的垃圾回收就發生在另一塊記憶體空間上。但這樣每次只使用一塊記憶體會造成記憶體浪費,HotSpot解決:將記憶體分為一塊較大的 eden 空間和兩塊較小的 survivor 空間,預設比例是 8:1:1,即每次新生代中可用記憶體空間為整個新生代容量的 90%,每次使 用 eden 和其中一個 survivour。當回收時,將 eden 和 survivor 中還存活的物件一次性複製到 另外一塊 survivor 上,最後清理掉 eden 和剛才用過的 survivor,若另外一塊 survivor 空間沒 有足夠記憶體空間存放上次新生代收集下來 的存活物件時,這些物件將直接通過分配擔保機制 進入老年代。

3)、標記-清理演算法:此方法主要是針對老年代,複製演算法不適合老年代,效率很低。標記過程和“標記-清除”演算法一樣,但後續步驟不是直接對可回收對 象進行清除,而是讓存活物件都向一端移動,然後直接清理掉另一端的記憶體

4)、分代收集演算法:根據記憶體的分代,選擇不同的垃圾回收演算法。新生代:停止-複製演算法     老年代:標記-清理演算法

垃圾回收器           

第一階段,Serial(序列)收集器 這個收集器是一個單執行緒收集器,只會使用一個CPU或者一條收集執行緒進行垃圾收集工作。其餘的工作執行緒必須暫停,直到收集結束,回收慢。PS:開啟Serial收集器的方式:-XX:+UseSerialGC

第二階段,Parallel(並行)收集器 Serial收集器的多執行緒版本。垃圾收集器執行緒和工作執行緒同時工作。效率高,但是當Heap過大時,應用程式暫停時間較長。PS:開啟Parallel收集器的方式:-XX:+UseParallelGC -XX:+UseParallelOldGC

第三階段,CMS(併發)收集器 CMS收集器在Minor GC時會暫停所有的應用執行緒,並以多執行緒的方式進行垃圾回收。在Full GC時不再暫停應用執行緒,而是使用若干個後臺執行緒定期的對老年代空間進行掃描,及時回收其中不再使用的物件。老年代回收時暫停時間短。產生記憶體碎片    PS:開啟CMS收集器的方式:-XX:+UseParNewGC -  XX:+UseConcMarkSweepGC

第四階段,G1(併發)收集器 G1收集器(或者垃圾優先收集器)的設計初衷是為了儘量縮短處理超大堆(大於4GB)時產生的停頓。相對於CMS的優勢而言是記憶體碎片的產生率大大降低。    PS:開啟G1收集器的方式:-XX:+UseG1GC    

類載入機制

類載入過程

類從載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括: 載入-驗證-準備-解析-初始化-使用-解除安裝,其中驗證-準備-解析稱為連結。

在載入階段,虛擬機器需要完成下面 3 件事:  1)通過一個類的全限定名獲取定義此類的二進位制位元組流; (可從檔案{class、jsp、jar檔案}、網路、計算生成一個二進位制流{$Proxy}、資料庫) 2)將這個位元組流所表示的靜態儲存結構轉化為方法區執行時資料結構  3)在方法區生成一個java.lang.Class物件,作為方法區資料的訪問入口。

驗證的目的是為了確保 clsss 檔案的位元組流中包含的資訊符合當前虛擬機器的要求,且 不會危害虛擬機器自身的安全。驗證階段大致會完成下面 4 個階段的檢驗動作: 1)檔案格式驗證  2)元資料驗證  3)位元組碼驗證  4)符號引用驗證{位元組碼驗證將對類的方法進行校驗分析,保證被校驗的方法在執行時不會做出危害虛擬機器的事,一個類方法體的位元組碼沒有通過 位元組碼驗證,那一定有問題,但若一個方法通過了驗證,也不能說明它一定安全}。 

準備階段是正式為類變數分配記憶體並設定變數的初始化值得階段,這些變數所使用的 記憶體都將在方法區中進行分配。(不是例項變數,且是初始值,若 public static int a=123;準 備階段後 a 的值為 0,而不是 123,要在初始化之後才變為 123,但若被 final 修飾,public static final int a=123;在準備階段後就變為了 123) 

解析階段是虛擬機器將常量池中的符號引用變為直接引用的過程。

初始化階段則是開始為變數附值

初始化和例項化不同:直接類名.a 類不會例項化,只會初始化

類載入器

從 Java 虛擬機器的角度來說,只存在兩種不同的類載入器:一種是啟動類載入器,這 個類載入器使用 c++實現,是虛擬機器自身的一部分。另一種就是所有其他的類載入器,這些 類載入器都由 Java 實現,且全部繼承自 java.lang.ClassLoader。

從 JAVA 開發人員角度,類載入器分為:  1)啟動類載入器,這個載入器負責把\lib 目錄中或者 –Xbootclasspath 下的類庫載入到虛擬機器記憶體中,啟動類載入器無法被 Java 程式直接引用。  2)擴充套件類載入器:負責載入\lib\ext 下或者 java.ext.dirs 系統變數指定 路徑下 all 類庫,開發者可以直接使用擴充套件類載入器。 3)應用程式類載入器,負責載入使用者路徑 classpath 上指定的類庫,開發者可以直接 使用這個類載入器,若應用程式中沒有定義過自己的類載入器,一般情況下,這個就是程式 中預設的類載入器。 4) 自定義載入器    要實現自定義類載入器:只需要繼承 java.lang.classLoader。 重寫loadClass方法

以上就是本次的內容。當然,整個JVM體系內容非常多,我只是將一些重點內容給提取出來而已,如果想詳細瞭解的話,還是推薦大家去看《深入理解Java虛擬機器》這本書籍。另外,本人才疏學淺,部分內容並沒有理解的非常透徹,如有錯誤部分請及時指出,本人感激不盡。