1. 程式人生 > >JVM 的 工作原理,層次結構 以及 GC工作原理

JVM 的 工作原理,層次結構 以及 GC工作原理

JVM

Java 虛擬機器 Java 虛擬機器(Java virtual machine,JVM)是執行 Java 程式必不可少的機制。JVM實現了Java語言最重要的特徵:即平臺無關性。原理:編譯後的 Java 程式指令並不直接在硬體系統的 CPU 上執行,而是由 JVM 執行。JVM遮蔽了與具體平臺相關的資訊,使Java語言編譯程式只需要生成在JVM上執行的目標位元組碼(.class),就可以在多種平臺上不加修改地執行。Java 虛擬機器在執行位元組碼時,把位元組碼解釋成具體平臺上的機器指令執行。因此實現java平臺無關性。它是 Java 程式能在多平臺間進行無縫移植的可靠保證,同時也是 Java 程式的安全檢驗引擎(還進行安全檢查)。

JVM 是 編譯後的 Java 程式(.class檔案)和硬體系統之間的介面 ( 編譯後:javac 是收錄於 JDK 中的 Java 語言編譯器。該工具可以將字尾名為. java 的原始檔編譯為字尾名為. class 的可以運行於 Java 虛擬機器的位元組碼。)

JVM = 類載入器 classloader + 執行引擎 execution engine + 執行時資料區域 runtime data area classloader 把硬碟上的class 檔案載入到JVM中的執行時資料區域, 但是它不負責這個類檔案能否執行,而這個是 執行引擎 負責的。

classloader

作用:裝載.class檔案 classloader 有兩種裝載class的方式 (時機):

  1. 隱式:執行過程中,碰到new方式生成物件時,隱式呼叫classLoader到JVM

  2. 顯式:通過class.forname()動態載入

雙親委派模型(Parent Delegation Model)

類的載入過程採用雙親委託機制,這種機制能更好的保證 Java 平臺的安全。 該模型要求除了頂層的Bootstrap class loader啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。子類載入器和父類載入器不是以繼承(Inheritance)的關係來實現,而是通過組合(Composition)關係來複用父載入器的程式碼。每個類載入器都有自己的名稱空間(由該載入器及所有父類載入器所載入的類組成,在同一個名稱空間中,不會出現類的完整名字(包括類的包名)相同的兩個類;在不同的名稱空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類)

雙親委派模型的工作過程為:

1.當前 ClassLoader 首先從自己已經載入的類中查詢是否此類已經載入,如果已經載入則直接返回原來已經載入的類。

每個類載入器都有自己的載入快取,當一個類被載入了以後就會放入快取,
等下次載入的時候就可以直接返回了。

2.當前 classLoader 的快取中沒有找到被載入的類的時候,委託父類載入器去載入,父類載入器採用同樣的策略,首先檢視自己的快取,然後委託父類的父類去載入,一直到 bootstrap ClassLoader.

  1. 當所有的父類載入器都沒有載入的時候,再由當前的類載入器載入,並將其放入它自己的快取中,以便下次有載入請求的時候直接返回。

使用這種模型來組織類載入器之間的關係的好處: 主要是為了安全性,避免使用者自己編寫的類動態替換 Java 的一些核心類,比如 String,同時也避免了重複載入,因為 JVM 中區分不同類,不僅僅是根據類名,相同的 class 檔案被不同的 ClassLoader 載入就是不同的兩個類,如果相互轉型的話會拋java.lang.ClassCaseException.

類載入器 classloader 是具有層次結構的,也就是父子關係。其中,Bootstrap 是所有類載入器的父親。如下圖所示:圖片描述Bootstrap class loader: 父類 當執行 java 虛擬機器時,這個類載入器被建立,它負責載入虛擬機器的核心類庫,如 java.lang.* 等。例如 java.lang.Object 就是由根類載入器載入的。需要注意的是,這個類載入器不是用 java 語言寫的,而是用 C/C++ 寫的。Extension class loader: 這個載入器加載出了基本 API 之外的一些拓展類。AppClass Loader: 載入應用程式和程式設計師自定義的類。

除了以上虛擬機器自帶的載入器以外,使用者還可以定製自己的類載入器(User-defined Class Loader)。Java 提供了抽象類 java.lang.ClassLoader,所有使用者自定義的類載入器應該繼承 ClassLoader 類。

這是JVM分工自治生態系統的一個很好的體現。

執行引擎

作用: 執行位元組碼,或者執行本地方法

runtime data area

JVM 執行時資料區 (JVM Runtime Area) 其實就是指 JVM 在執行期間,其對JVM記憶體空間的劃分和分配。JVM在執行時將資料劃分為了6個區域來儲存。

程式設計師寫的所有程式都被載入到執行時資料區域中,不同類別存放在heap, java stack, native method stack, PC register, method area.

下面對各個部分的功能和儲存的內容進行描述:

clipboard.png

1、PC程式計數器:一塊較小的記憶體空間,可以看做是當前執行緒所執行的位元組碼的行號指示器, NAMELY儲存每個執行緒下一步將執行的JVM指令,如該方法為native的,則PC暫存器中不儲存任何資訊。Java 的多執行緒機制離不開程式計數器,每個執行緒都有一個自己的PC,以便完成不同執行緒上下文環境的切換。

2、java虛擬機器棧:與 PC 一樣,java 虛擬機器棧也是執行緒私有的。每一個 JVM 執行緒都有自己的 java 虛擬機器棧,這個棧與執行緒同時建立,它的生命週期與執行緒相同。虛擬機器棧描述的是Java 方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法被呼叫直至執行完成的過程就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程

3、本地方法棧:與虛擬機器棧的作用相似,虛擬機器棧為虛擬機器執行執行java方法服務,而本地方法棧則為虛擬機器使用到的本地方法服務。

4、Java堆:被所有執行緒共享的一塊儲存區域,在虛擬機器啟動時建立,它是JVM用來儲存物件例項以及陣列值的區域,可以認為Java中所有通過new建立的物件的記憶體都在此分配。

Java堆在JVM啟動的時候就被建立,堆中儲存了各種物件,這些物件被自動管理記憶體系統(Automatic Storage Management System,也即是常說的 “Garbage Collector(垃圾回收器)”)所管理。這些物件無需、也無法顯示地被銷燬。

JVM將Heap分為兩塊:新生代New Generation和舊生代Old Generation

Note:

  • 堆在JVM是所有執行緒共享的,因此在其上進行物件記憶體的分配均需要進行加鎖,這也是new開銷比較大的原因。

  • 鑑於上面的原因,Sun Hotspot JVM為了提升物件記憶體分配的效率,對於所建立的執行緒都會分配一塊獨立的空間,這塊空間又稱為TLAB

  • TLAB僅作用於新生代的Eden Space,因此在編寫Java程式時,通常多個小的物件比大的物件分配起來更加高效

5、方法區 方法區和堆區域一樣,是各個執行緒共享的記憶體區域,它用於儲存每一個類的結構資訊,例如執行時常量池,成員變數和方法資料,建構函式和普通函式的位元組碼內容,還包括一些在類、例項、介面初始化時用到的特殊方法。當開發人員在程式中通過Class物件中的getName、isInstance等方法獲取資訊時,這些資料都來自方法區。

方法區也是全域性共享的,在虛擬機器啟動時候建立。在一定條件下它也會被GC。這塊區域對應Permanent Generation 持久代。 XX:PermSize指定大小。

6、執行時常量池 其空間從方法區中分配,存放的為類中固定的常量資訊、方法和域的引用資訊。

GC

Java garbage collection is an automatic process to manage the runtime memory used by programs. By doing it automatic JVM relieves the programmer of the overhead of assigning and freeing up memory resources in a program. java 與 C語言相比的一個優勢是,可以通過自己的JVM自動分配和回收記憶體空間。

何為GC? 垃圾回收機制是由垃圾收集器Garbage Collection GC來實現的,GC是後臺的守護程序。它的特別之處是它是一個低優先順序程序,但是可以根據記憶體的使用情況動態的調整他的優先順序。因此,它是在記憶體中低到一定限度時才會自動執行,從而實現對記憶體的回收。這就是垃圾回收的時間不確定的原因。

為何要這樣設計:因為GC也是程序,也要消耗CPU等資源,如果GC執行過於頻繁會對java的程式的執行產生較大的影響(java直譯器本來就不快),因此JVM的設計者們選著了不定期的gc。

GC有關的是: runtime data area 中的 heap(物件例項會儲存在這裡) 和 gabage collector方法。 程式執行期間,所有物件例項儲存在執行時資料區域的heap中,當一個物件不再被引用(使用),它就需要被收回。在GC過程中,這些不再被使用的物件從heap中收回,這樣就會有空間被迴圈利用。 GC為記憶體中不再使用的物件進行回收,GC中呼叫回收的方法--收集器garbage collector. 由於GC要消耗一些資源和時間,Java 在對物件的生命週期特徵(eden or survivor)進行分析之後,採用了分代的方式進行物件的收集,以縮短GC對應用造成的暫停。

在垃圾回收器回收記憶體之前,還需要一些清理工作。 因為垃圾回收gc只能回收通過new關鍵字申請的記憶體(在堆上),但是堆上的記憶體並不完全是通過new申請分配的。還有一些本地方法(一般是呼叫的C方法)。這部分“特殊的記憶體”如果不手動釋放,就會導致記憶體洩露,gc是無法回收這部分記憶體的。 所以需要在finalize中用本地方法(native method)如free操作等,再使用gc方法。顯示的GC方法是system.gc()

垃圾回收技術

方法一:引用計數法。簡單但速度很慢。缺陷是:不能處理迴圈引用的情況。 方法二:停止-複製(stop and copy)。效率低,需要的空間大,優點,不會產生碎片。 方法三:標記 - 清除演算法 (mark and sweep)。速度較快,佔用空間少,標記清除後會產生大量的碎片。

JAVA虛擬機器中是如何做的?  java的做法很聰明,我們稱之為"自適應"的垃圾回收器,或者是"自適應的、分代的、停止-複製、標記-清掃"式垃圾回收器。它會根據不同的環境和需要選擇不同的處理方式。

heap組成

由於GC需要消耗一些資源和時間的,Java在對物件的生命週期特徵進行分析後,採用了分代的方式來進行物件的收集,即按照新生代、舊生代的方式來對物件進行收集,以儘可能的縮短GC對應用造成的暫停. heap 的組成有三區域/世代:(可以理解隨著時間,物件例項不斷變換heap中的等級,有點像年級)

  1. 新生代 Young Generation

    1. Eden Space 任何新進入執行時資料區域的例項都會存放在此

    2. S0 Suvivor Space 存在時間較長,經過垃圾回收沒有被清除的例項,就從Eden 搬到了S0

    3. S1 Survivor Space 同理,存在時間更長的例項,就從S0 搬到了S1

  2. 舊生代 Old Generation/tenured

    同理,存在時間更長的例項,物件多次回收沒被清除,就從S1 搬到了tenured
  3. Perm 存放執行時資料區的方法區

Java 不同的世代使用不同的 GC 演算法。

  1. Minor collection: 新生代 Young Generation 使用將 Eden 還有 Survivor 內的資料利用 semi-space 做複製收集(Copying collection), 並將原本 Survivor 內經過多次垃圾收集仍然存活的物件移動到 Tenured。

  2. Major collection 則會進行 Minor collection,Tenured 世代則進行標記壓縮收集。

圖片描述 To note that:

 這個搬運工作都是GC 完成的,這也是garbage collector 的名字來源,而不是叫garbage cleaner. GC負責在heap中搬運例項,以及收回儲存空間。

GC工作原理

JVM 分別對新生代和舊生代採用不同的垃圾回收機制

何為垃圾?

Java中那些不可達的物件就會變成垃圾。那麼什麼叫做不可達?其實就是沒有辦法再引用到該物件了。主要有以下情況使物件變為垃圾: 1.對非執行緒的物件來說,所有的活動執行緒都不能訪問該物件,那麼該物件就會變為垃圾。 2.對執行緒物件來說,滿足上面的條件,且執行緒未啟動或者已停止。

例如: 
(1)改變物件的引用,如置為null或者指向其他物件。 
   Object x=new Object();//object1 
   Object y=new Object();//object2 
   x=y;//object1 變為垃圾 
   x=y=null;//object2 變為垃圾 

(2)超出作用域 
   if(i==0){ 
      Object x=new Object();//object1 
   }//括號結束後object1將無法被引用,變為垃圾 
(3)類巢狀導致未完全釋放 
   class A{ 
      A a; 
   } 
   A x= new A();//分配一個空間 
   x.a= new A();//又分配了一個空間 
   x=null;//將會產生兩個垃圾 
(4)執行緒中的垃圾 
   class A implements Runnable{   
     void run(){ 
       //.... 
     } 
   } 
   //main 
   A x=new A();//object1 
   x.start(); 
   x=null;//等執行緒執行完後object1才被認定為垃圾 
   這樣看,確實在程式碼執行過程中會產生很多垃圾,不過不用擔心,java可以有效地處理他們。

JVM中將物件的引用分為了四種類型,不同的物件引用型別會造成GC採用不同的方法進行回收: (1)強引用:預設情況下,物件採用的均為強引用

(GC不會回收)

(2)軟引用:軟引用是Java中提供的一種比較適合於快取場景的應用

(只有在記憶體不夠用的情況下才會被GC)

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