1. 程式人生 > >0.1.JVM&垃圾回收

0.1.JVM&垃圾回收

JVM(Java Virtual Machine,Java虛擬機器)

    Java程式的跨平臺特性主要是指位元組碼檔案可以在任何具有Java虛擬機器的計算機或者電子裝置上執行,Java虛擬機器中的Java直譯器負責將位元組碼檔案解釋成為特定的機器碼進行執行。因此在執行時,Java源程式需要通過編譯器編譯成為.class檔案。眾所周知java.exe是java class檔案的執行程式,但實際上java.exe程式只是一個執行的外殼,它會裝載jvm.dll(windows下,下皆以windows平臺為例,linux下和solaris下其實類似,為:libjvm.so),這個動態連線庫才是java虛擬機器的實際操作處理所在。

    JVM是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。JVM有自己完善的硬體架構,如處理器、堆疊、暫存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平臺執行。使用JVM就是為了支援與作業系統無關,實現跨平臺。所以,JAVA虛擬機器JVM是屬於JRE的,而現在我們安裝JDK時也附帶安裝了JRE(當然也可以單獨安裝JRE)。

JVM 允許一個應用併發執行多個執行緒。Hotspot JVM 中的 Java 執行緒與原生作業系統執行緒有直接的對映關係。當執行緒本地儲存、緩衝區分配、同步物件、棧、程式計數器等準備好以後,就會建立一個作業系統原生執行緒。Java 執行緒結束,原生執行緒隨之被回收。作業系統負責排程所有執行緒,並把它們分配到任何可用的 CPU 上。當原生執行緒初始化完畢,就會呼叫 Java 執行緒的 run() 方法。run() 返回時,被處理未捕獲異常,原生執行緒將確認由於它的結束是否要終止 JVM 程序(比如這個執行緒是最後一個非守護執行緒)。當執行緒結束時,會釋放原生執行緒和 Java 執行緒的所有資源。

java執行時環境(JRE)

    java執行時環境是JVM的一個超集。JVM對於一個平臺或者作業系統是明確的,而JRE確實一個一般的概念,他代表了完整的執行時環境。我們在jre資料夾中看到的所有的jar檔案和可執行檔案都會變成執行時的一部分。事實上,執行時JRE變成了JVM。所以對於一般情況時候使用JRE,對於明確的作業系統來說使用JVM。當你下載了JRE的時候,也就自動下載了JVM。

java開發工具箱(JDK)

    java開發工具箱指的是編寫一個java應用所需要的所有jar檔案和可執行檔案。事實上,JRE是JKD的一部分。如果你下載了JDK,你會看到一個名叫JRE的資料夾在裡面。JDK中要被牢記的jar檔案就是tools.jar,它包含了用於執行java文件的類還有用於類簽名的jar包。

即時編譯器(JIT)

    即時編譯器是種特殊的編譯器,它通過有效的把位元組碼變成機器碼來提高JVM的效率。JIT這種功效很特殊,因為他把檢測到的相似的位元組碼編譯成單一執行的機器碼,從而節省了CPU的使用。這和其他的位元組碼編譯器不同,因為他是執行時(從位元組碼到機器碼)而不是在程式執行之前。正是因為這些,動態編譯這個詞彙才和JIT有那麼緊密的關係。

JVM記憶體區域劃分

粗略分來,JVM的內部體系結構分為三部分,分別是:類裝載器(ClassLoader)子系統,執行時資料區,和執行引擎。

類裝載器

    每一個Java虛擬機器都由一個類載入器子系統(class loader subsystem),負責載入程式中的型別(類和介面),並賦予唯一的名字。每一個Java虛擬機器都有一個執行引擎(execution engine)負責執行被載入類中包含的指令。JVM的兩種類裝載器包括:啟動類裝載器和使用者自定義類裝載器,啟動類裝載器是JVM實現的一部分,使用者自定義類裝載器則是Java程式的一部分,必須是ClassLoader類的子類。

JVM 中有多個類載入器,分飾不同的角色。每個類載入器由它的父載入器載入。bootstrap 載入器除外,它是所有最頂層的類載入器。

  • Bootstrap 載入器 一般由原生代碼實現,因為它在 JVM 載入以後的早期階段就被初始化了。bootstrap 載入器負責載入基礎的 Java API,JRE/lib/rt.jar。它只加載擁有較高信任級別的啟動路徑下找到的類,因此跳過了很多普通類需要做的校驗工作。
  • Extension 載入器 載入了標準 Java 擴充套件 API 中的類,比如 security 的擴充套件函式。JRE/lib/ext或者java.ext.dirs指向的目錄(Class.forName())
  • Application類載入器 – CLASSPATH環境變數, 由-classpath或-cp選項定義,或者是JAR中的Manifest的classpath屬性定義.(System 載入器 是應用的預設類載入器,比如從 classpath 中載入應用類)

使用者自定義類載入器 也可以用來載入應用類。使用自定義的類載入器有很多特殊的原因:執行時重新載入類或者把載入的類分隔為不同的組,典型的用法比如 web 伺服器 Tomcat。

類載入器的工作原理

類載入器的工作原理基於三個機制:委託、可見性和單一性。類載入器使用委託機制的工作原理。

委託機制

當一個類載入和初始化的時候,類僅在有需要載入的時候被載入。假設你有一個應用需要的類叫作Abc.class,首先載入這個類的請求由Application類載入器委託給它的父類載入器Extension類(延伸)載入器,然後再委託給Bootstrap類載入器。Bootstrap類載入器會先看看rt.jar中有沒有這個類,因為並沒有這個類,所以這個請求由回到Extension類載入器,它會檢視jre/lib/ext目錄下有沒有這個類,如果這個類被Extension類載入器找到了,那麼它將被載入,而Application類載入器不會載入這個類;而如果這個類沒有被Extension類載入器找到,那麼再由Application類載入器從classpath中尋找。記住classpath定義的是類檔案的載入目錄,而PATH是定義的是可執行程式如javac,java等的執行路徑。

可見性機制

根據可見性機制,子類載入器可以看到父類載入器載入的類,而反之則不行。所以下面的例子中,當Abc.class已經被Application類載入器載入過了,然後如果想要使用Extension類載入器載入這個類,將會丟擲java.lang.ClassNotFoundException異常。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

package test;

 

import java.util.logging.Level;

import java.util.logging.Logger;

 

/**

* Java program to demonstrate How ClassLoader works in Java,

* in particular about visibility principle of ClassLoader.

*

* @author Javin Paul

*/

 public class ClassLoaderTest {

 public static void main(String args[]) {

            try {         

                //printing ClassLoader of this class

                System.out.println("ClassLoaderTest.getClass().getClassLoader() : "

                                     + ClassLoaderTest.class.getClassLoader());

 

                //trying to explicitly load this class again using Extension class loader

                Class.forName("test.ClassLoaderTest", true

                                ,  ClassLoaderTest.class.getClassLoader().getParent());

            } catch (ClassNotFoundException ex) {

                Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex);

            }

        }

 

    }

單一性機制

根據這個機制,父載入器載入過的類不能被子載入器載入第二次。雖然重寫違反委託和單一性機制的類載入器是可能的,但這樣做並不可取。你寫自己的類載入器的時候應該嚴格遵守這三條機制。

如何顯式的載入類

Java提供了顯式載入類的API:Class.forName(classname)和Class.forName(classname, initialized, classloader)。就像上面的例子中,你可以指定類載入器的名稱以及要載入的類的名稱。類的載入是通過呼叫java.lang.ClassLoader的loadClass()方法,而loadClass()方法則呼叫了findClass()方法來定位相應類的位元組碼。在這個例子中Extension類載入器使用了java.net.URLClassLoader,它從JAR和目錄中進行查詢類檔案,所有以”/”結尾的查詢路徑被認為是目錄。如果findClass()沒有找到那麼它會丟擲java.lang.ClassNotFoundException異常,而如果找到的話則會呼叫defineClass()將位元組碼轉化成類例項,然後返回。

執行引擎:它或者在執行位元組碼,或者執行本地方法

    主要的執行技術有:解釋,即時編譯,自適應優化、晶片級直接執行其中解釋屬於第一代JVM,即時編譯JIT屬於第二代JVM,自適應優化(目前Sun的HotspotJVM採用這種技術)則吸取第一代JVM和第二代JVM的經驗,採用兩者結合的方式 。

    自適應優化:開始對所有的程式碼都採取解釋執行的方式,並監視程式碼執行情況,然後對那些經常呼叫的方法啟動一個後臺執行緒,將其編譯為原生代碼,並進行仔細優化。若方法不再頻繁使用,則取消編譯過的程式碼,仍對其進行解釋執行。

執行時資料區:主要包括:方法區,堆,Java棧,PC暫存器,本地方法棧

  • 方法區和堆由所有執行緒共享

    堆:它是JVM用來儲存物件例項以及陣列值的區域,可以認為Java中所有通過new建立的物件的記憶體都在此分配,Heap中的物件的記憶體需要等待GC進行回收。

為了支援垃圾回收機制,堆被分為了下面三個區域:

  • 新生代
    • 經常被分為 Eden 和 Survivor
  • 老年代
  • 永久代

記憶體管理

通常我們說的JVM記憶體回收總是在指堆記憶體回收,確實只有堆中的內容是動態申請分配的,所以以上物件的年輕代和年老代都是指的JVM的Heap空間,而持久代則是之前提到的MethodArea,不屬於Heap。GC的基本原理:將記憶體中不再被使用的物件進行回收,GC中用於回收的方法稱為收集器,由於GC需要消耗一些資源和時間,Java在對物件的生命週期特徵進行分析後,按照新生代、舊生代的方式來對物件進行收集,以儘可能的縮短GC對應用造成的暫停

物件和陣列永遠不會顯式回收,而是由垃圾回收器自動回收。通常,過程是這樣的:

(1)對新生代的物件的收集稱為minor GC;

(2)對舊生代的物件的收集稱為Full GC;

  •  新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因為 Java 物件大多都具

備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。

  •  老年代 GC(Major GC  / Full GC):指發生在老年代的 GC,出現了 Major GC,經常

會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裡,就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10倍以上。

Minor GC觸發機制:

當年輕代滿時就會觸發Minor GC,這裡的年輕代滿指的是Eden代滿,Survivor滿不會引發GC

Full GC觸發機制:

當年老代滿時會引發Full GC,Full GC將會同時回收年輕代、年老代,

當永久代滿時也會引發Full GC,會導致Class、Method元資訊的解除安裝

(3)程式中主動呼叫System.gc()強制執行的GC為Full GC。

    不同的引用型別, GC會採用不同的方法進行回收,JVM物件的引用分為了四種類型:

(1)強引用:預設情況下,物件採用的均為強引用(這個物件的例項沒有其他物件引用

,GC時才會被回收)

(2)軟引用:軟引用是Java中提供的一種比較適合於快取場景的應用(只有在記憶體不夠用

的情況下才會被GC)

(3)弱引用:在GC時一定會被GC回收

(4)虛引用:由於虛引用只是用來得知物件是否被GC

  • Young(年輕代)虛擬機器給每個物件定義了一個物件年齡(Age)計數器。

    年輕代分三個區。一個Eden區,兩個Survivor區。大部分物件在Eden區中生成。當Eden區滿時,還存活的物件將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活物件將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的物件,將被複制年老區(Tenured。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來物件,和從前一個Survivor複製過來的物件,而複製到年老區的只有從第一個Survivor去過來的物件。而且,Survivor區總有一個是空的。

  • Tenured(年老代)

    年老代存放從年輕代存活的物件。一般來說年老代存放的都是生命期較長的物件。

  • Perm(持久代)

    用於存放靜態檔案,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者呼叫一些class,例如Hibernate等,在這種時候需要設定一個比較大的持久代空間來存放這些執行過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設定。

(Java有四種類型的垃圾回收器:

1.序列垃圾回收器(Serial Garbage Collector)

2.並行垃圾回收器(Parallel Garbage Collector)

3.併發標記掃描垃圾回收器(CMS Garbage Collector)

4.G1垃圾回收器(G1 Garbage Collector)

1、序列垃圾回收器

序列垃圾回收器通過持有應用程式所有的執行緒進行工作。它為單執行緒環境設計,只使用一個單獨的執行緒進行垃圾回收,通過凍結所有應用程式執行緒進行工作,所以可能不適合伺服器環境。它最適合的是簡單的命令列程式。通過JVM引數-XX:+UseSerialGC可以使用序列垃圾回收器。

2、並行垃圾回收器

並行垃圾回收器也叫做 throughput collector 。它是JVM的預設垃圾回收器。與序列垃圾回收器不同,它使用多執行緒進行垃圾回收。相似的是,也會凍結所有的應用程式執行緒當執行垃圾回收的時候

3、併發標記掃描垃圾回收器

併發標記垃圾回收使用多執行緒掃描堆記憶體,標記需要清理的例項並且清理被標記過的例項。併發標記垃圾回收器只會在下面兩種情況持有應用程式所有執行緒。

1.當標記的引用物件在tenured區域;2.在進行垃圾回收的時候,堆記憶體的資料被併發的改變。

相比並行垃圾回收器,併發標記掃描垃圾回收器使用更多的CPU來確保程式的吞吐量。如果我們可以為了更好的程式效能分配更多的CPU,那麼併發標記上掃描垃圾回收器是更好的選擇相比並發垃圾回收器。通過JVM引數 XX:+USeParNewGC 開啟併發標記掃描垃圾回收器。

4、G1垃圾回收器

G1垃圾回收器適用於堆記憶體很大的情況,他將堆記憶體分割成不同的區域,並且併發的對其進行垃圾回收。G1也可以在回收記憶體之後對剩餘的堆記憶體空間進行壓縮。併發掃描標記垃圾回收器在STW情況下壓縮記憶體。G1垃圾回收會優先選擇第一塊垃圾最多的區域。通過JVM引數 –XX:+UseG1GC 使用G1垃圾回收器)

非堆記憶體

非堆記憶體指的是那些邏輯上屬於 JVM 一部分物件,但實際上不在堆上建立。

非堆記憶體包括:

  • 永久代,包括:
      • 方法區
      • 駐留字串(interned strings)
    • 程式碼快取(Code Cache):用於編譯和儲存那些被 JIT 編譯器編譯成原生程式碼的方法。

    方法區:當JVM的類裝載器載入.class檔案,並進行解析,把解析的型別資訊放入方法區。方法區域存放了所載入的類的資訊(名稱、修飾符等)、類中的靜態變數、類中定義為final型別的常量、類中的Field資訊、類中的方法資訊,當開發人員在程式中通過Class物件中的getName、isInterface等方法來獲取資訊時,這些資料都來源於方法區域,同時方法區域也是全域性共享的,在一定的條件下它也會被GC,當方法區域需要使用的記憶體超過其允許的大小時,會丟擲OutOfMemory的錯誤資訊。

  • Java棧和PC暫存器由執行緒獨享

    JVM棧是執行緒私有的,每個執行緒建立的同時都會建立JVM棧,JVM棧中存放的為當前執行緒中區域性基本型別的變數(java中定義的八種基本型別:boolean、char、byte、short、int、long、float、double)、部分的返回結果以及Stack Frame,非基本型別的物件在JVM棧上僅存放一個指向堆上的地址

本地方法棧:儲存本地方法呼叫的狀態

程式計數器 : 程式計數器(Program Counter Register)是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器的概念模型裡(僅是概念模型,各種虛擬機器可能會通過一些更高效的方式去實現),位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。由於Java 虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。如果執行緒正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空(Undefined)。此記憶體區域是唯一一個在Java 虛擬機器規範中沒有規定任何OutOfMemoryError 情況的區域。