1. 程式人生 > >沉澱再出發:jvm的本質

沉澱再出發:jvm的本質

沉澱再出發:jvm的本質

一、前言

    關於jvm,使用的地方實在是太多了,從字面意思上我們都能明白這也是一個虛擬機器,那麼其他的虛擬機器都會用來執行別的作業系統的,而jvm卻是實現了可以在不用的作業系統之上運行同樣的位元組碼檔案,以此來實現程式碼的可移植性,大家可以看一下編譯原理,就知道了jvm執行程式碼的本質其實是根據不同的平臺將位元組碼檔案(中間程式碼)變成最終適合不同平臺的機器碼。同時jvm中也有很多的概念,肯定也是和編譯系統相關的了,資料和程式碼如何儲存,資料分為哪幾種資料,需要什麼格式儲存,堆疊等等,以及相關資料的生存週期,垃圾回收機制,由此產生的一系列的問題,函式的儲存和呼叫,理解到這個程度,我們就能更好地理解使用java進行開發的其他軟體,比如hadoop等等。同樣的,對於程序和執行緒的儲存和執行的情況,併發以及volatile我們都有了更深的理解。

二、jvm初探

 2.1、java平臺

     Java平臺由Java虛擬機器和Java應用程式介面搭建,Java語言則是進入這個平臺的通道,用Java語言編寫並編譯的程式可以執行在這個平臺上。這個平臺的結構如下圖所示:

 2.2、JVM體系結構

  1) 類裝載器(ClassLoader)(用來裝載.class檔案)
  2) 執行引擎(執行位元組碼,或者執行本地方法)
  3) 執行時資料區(方法區、堆、java棧、PC暫存器、本地方法棧)

 2.3、JVM生命週期

1  啟動:啟動一個Java程式時,一個JVM例項就產生了,任何一個擁有public static
void main(String[] args)函式的class都可以作為JVM例項執行的起點。 2 執行:main()作為該程式初始執行緒的起點,任何其他執行緒均由該執行緒啟動。 3 消亡:當程式中的所有非守護執行緒都終止時,JVM才退出;若安全管理器允許,程式也可以使用Runtime類或者System.exit()來退出。

  一個執行中的Java虛擬機器有著清晰的任務:執行Java程式。程式開始執行時才執行,程式結束時就停止。在同一臺機器上執行三個程式,就會有三個執行中的Java虛擬機器。 Java虛擬機器總是開始於一個main()方法,這個方法必須是公有、返回void、只接受一個字串陣列。在程式執行時,必須給Java虛擬機器指明main()方法的類名。main()方法是程式的起點,執行的執行緒初始化為初始執行緒,程式中其他的執行緒都由其來啟動。


  Java中的執行緒分為兩種:守護執行緒 (daemon)和普通執行緒(non-daemon)。守護執行緒是Java虛擬機器自己使用的執行緒,比如負責垃圾收集的執行緒就是一個守護執行緒。當然,也可以把自己的程式設定為守護執行緒,包含main()方法的初始執行緒不是守護執行緒。只要Java虛擬機器中還有普通的執行緒在執行,Java虛擬機器就不會停止。如果有足夠的許可權,可以呼叫exit()方法終止程式。

 2.4、 JVM執行時資料區

 

 Java堆(Heap)

    被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立用來儲存物件例項,可以通過-Xmx和-Xms控制堆的大小;OutOfMemoryError異常:當在堆中沒有記憶體完成例項分配,且堆也無法再擴充套件時。java堆是垃圾收集器管理的主要區域。java堆還可以細分為:新生代(New/Young)、舊生代/年老代(Old/Tenured)持久代(Permanent)在方法區,不屬於Heap。

1 新生代:新建的物件都由新生代分配記憶體。常常又被劃分為Eden區和Survivor區。Eden空間不足時會把存活的物件轉移到Survivor。新生代的大小可由-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。
2 舊生代:存放經過多次垃圾回收仍然存活的物件。
3 持久代:存放靜態檔案,如Java類、方法等;持久代在方法區,對垃圾回收沒有顯著影響。

 方法區

    執行緒間共享,用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器(JIT)編譯後的程式碼等資料;OutOfMemoryError異常:當方法區無法滿足記憶體的分配需求時。JVM用持久代(Permanet Generation)來存放方法區。
    執行時常量池:方法區的一部分,用於存放編譯期生成的各種字面量與符號引用,如String型別常量就存放在常量池;OutOfMemoryError異常:當常量池無法再申請到記憶體時。

java虛擬機器棧(VM Stack)

    執行緒私有,生命週期與執行緒相同;儲存方法的區域性變量表(基本型別、物件引用)、運算元棧、動態連結、方法出口等資訊。 java方法執行的記憶體模型,每個方法執行的同時都會建立一個棧幀,每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

1     StackOverflowError異常:當執行緒請求的棧深度大於虛擬機器所允許的深度
2     OutOfMemoryError異常:如果棧的擴充套件時無法申請到足夠的記憶體

   JVM棧是執行緒私有的,每個執行緒建立的同時都會建立JVM棧,JVM棧中存放的為當前執行緒中區域性基本型別的變數、部分的返回結果以及Stack Frame。其他引用型別的物件在JVM棧上僅存放變數名和指向堆上物件例項的首地址。

 本地方法棧(Native Method Stack)

    與虛擬機器棧相似,主要為虛擬機器使用到的Native方法服務,在HotSpot虛擬機器中直接把本地方法棧與虛擬機器棧二合一。用於支援native方法的執行,儲存了每個native方法呼叫的狀態。對於本地方法介面,實現JVM並不要求一定要有它的支援,甚至可以完全沒有。Sun公司實現Java本地介面(JNI)是出於可移植性的考慮,當然我們也可以設計出其它的本地介面來代替Sun公司的JNI。但是這些設計與實現是比較複雜的事情,需要確保垃圾回收器不會將那些正在被本地方法呼叫的物件釋放掉。

 程式計數器(Program Counter Register)

     當前執行緒所執行的位元組碼的行號指示器當前執行緒私有,不會出現OutOfMemoryError情況。程式計數器是一塊較小的記憶體空間,它的作用可以看作是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器的概念模型裡位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
   如果執行緒正在執行的是Java 方法,則這個計數器記錄的是正在執行的虛擬機器位元組碼指令地址;
    如果正在執行的是Native 方法,則這個技術器值為空(Undefined);程式計數器記錄的位元組碼指令地址,但是native 本地(如:System.currentTimeMillis()/ public static native long currentTimeMillis();)方法是大多是通過C實現並未編譯成需要執行的位元組碼指令所以在計數器中當然是空(undefined)。

   讓我們通過一個例子來看:

 1 public class ZyrCal { 
 2     public static void main(String [] args){
 3              System.out.println(calc());
 4     }
 5     public static int calc(){ 
 6        int a = 100;
 7        int b = 200;
 8        int c = 300; 
 9        return ( a + b ) * c; 
10     }
11  }

 native 方法的多執行緒實現方式:   

   native 方法是通過呼叫系統指令來實現的,那系統是如何實現多執行緒的,native 就是如何實現的。Java執行緒總是需要以某種形式對映到OS執行緒上,對映模型可以是1:1(原生執行緒模型)、n:1(綠色執行緒 / 使用者態執行緒模型)、m:n(混合模型)。以HotSpot VM的實現為例,它目前在大多數平臺上都使用1:1模型,也就是每個Java執行緒都直接對映到一個OS執行緒上執行。此時,native方法就由原生平臺直接執行,並不需要理會抽象的JVM層面上的“pc暫存器”概念,原生的CPU上真正的PC暫存器是怎樣就是怎樣,就像一個用C或C++寫的多執行緒程式。

 直接記憶體(Direct Memory)

    直接記憶體並不是虛擬機器執行的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁使用;NIO可以使用Native函式庫直接分配堆外記憶體,堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。大小不受Java堆大小的限制,受本機(伺服器)記憶體限制。OutOfMemoryError異常:系統記憶體不足時。

Java物件例項存放在堆中;
常量存放在方法區的常量池;
虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料放在方法區;
以上區域是所有執行緒共享的。
棧是執行緒私有的,存放該方法的區域性變量表(基本型別、物件引用)、運算元棧、動態連結、方法出口等資訊。
一個Java程式對應一個JVM,一個方法(執行緒)對應一個Java棧。

 2.4、Java程式碼的編譯和執行過程

 Java程式碼的編譯和執行包括了三個重要機制:

(1)Java原始碼編譯機制(.java原始碼檔案 -> .class位元組碼檔案)
(2)類載入機制(ClassLoader)
(3)類執行機制(JVM執行引擎)

 2.4.1、Java原始碼編譯機制

  Java原始碼是不能被機器識別的,需要先經過編譯器編譯成JVM可以執行的.class位元組碼檔案,再由直譯器解釋執行。即Java原始檔(.java) -- Java編譯器 --> Java位元組碼檔案 (.class) --> Java直譯器 --> 執行。流程圖如下:

    位元組碼檔案(.class)是平臺無關的。Java中字元只以Unicode一種形式存在。字元轉換髮生在JVM和OS交界處(Reader/Writer)。最後生成的class檔案由以下部分組成:

1  結構資訊:包括class檔案格式版本號及各部分的數量與大小的資訊
2  元資料:對應於Java原始碼中宣告與常量的資訊。包含類/繼承的超類/實現的介面的宣告資訊、域與方法宣告資訊和常量池
3  方法資訊:對應Java原始碼中語句和表示式對應的資訊。包含位元組碼、異常處理器表、求值棧與區域性變數區大小、求值棧的型別記錄、除錯符號資訊

 2.4.2、類載入機制(ClassLoader)

  Java程式是由多個獨立的類檔案組成。這些類檔案並非一次性全部裝入記憶體,而是依據程式逐步載入。JVM的類載入是通過ClassLoader及其子類來完成的,類的層次關係和載入順序可以由下圖來描述:

 1 1、Bootstrap ClassLoader
 2     JVM的根ClassLoader,由C++實現
 3     載入Java的核心API:$JAVA_HOME中jre/lib/rt.jar中所有class檔案的載入,這個jar中包含了java規範定義的所有介面以及實現。
 4     JVM啟動時即初始化此ClassLoader
 5 2、Extension ClassLoader
 6      載入Java擴充套件API(lib/ext中的類)
 7 3、App ClassLoader
 8     載入Classpath目錄下定義的class
 9 4、Custom ClassLoader
10     屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據J2EE規範自行實現ClassLoader。
1 雙親委派機制:JVM在載入類時預設採用的是雙親委派機制。通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴。如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。
2   作用:1)避免重複載入;2)更安全。如果不是雙親委派,那麼使用者在自己的classpath編寫了一個java.lang.Object的類,那就無法保證Object的唯一性。所以使用雙親委派,即使自己編寫了,但是永遠都不會被載入執行。
3 
4 破壞雙親委派機制:雙親委派機制並不是一種強制性的約束模型,而是Java設計者推薦給開發者的類載入器實現方式。執行緒上下文類載入器,這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,那麼這個類載入器就是應用程式類載入器。像JDBC就是採用了這種方式。這種行為就是逆向使用了載入器,違背了雙親委派模型的一般性原則。

 2.4.3、類執行機制

   Java位元組碼的執行是由JVM執行引擎來完成,流程圖如下所示:

    JVM是基於棧的體系結構來執行class位元組碼的,執行緒建立後,都會產生程式計數器(PC)和棧(Stack),程式計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每個棧幀對應著每個方法的每次呼叫,而棧幀又是有區域性變數區和運算元棧兩部分組成,區域性變數區用於存放方法中的區域性變數和引數,運算元棧中用於存放方法執行過程中產生的中間結果。

 主要的執行技術:解釋,即時編譯,自適應優化、晶片級直接執行

1     解釋屬於第一代JVM,
2     即時編譯JIT屬於第二代JVM,
3     自適應優化(目前Sun的HotspotJVM採用這種技術)則吸取第一代JVM和第二代JVM的經驗,採用兩者結合的方式
4     開始對所有的程式碼都採取解釋執行的方式,並監視程式碼執行情況。
對那些經常呼叫的方法啟動一個後臺執行緒,將其編譯為原生代碼,並進行優化。
若方法不再頻繁使用,則取消編譯過的程式碼,仍對其進行解釋執行。

 2.5、JVM垃圾回收(GC)

    GC的基本原理:將記憶體中不再被引用的物件(垃圾)進行回收,GC中用於回收的方法稱為收集器。由於GC需要消耗一些資源和時間,Java在對物件的生命週期特徵進行分析後,按照新生代、舊生代的方式來對物件進行收集,以儘可能的縮短GC對應用造成的暫停。

1 對新生代的物件的收集稱為minor GC;
2 對舊生代的物件的收集稱為Full GC;
3 程式中主動呼叫System.gc()的GC為Full GC。 

   Java垃圾回收是單獨的後臺執行緒gc執行的,自動執行無需顯示呼叫。即使主動呼叫了java.lang.System.gc(),該方法也只會提醒系統進行垃圾回收,但系統不一定會迴應,可能會不予理睬。
   判斷一塊記憶體空間是否符合回收標準:

(1)物件賦予了空值,且之後再未呼叫(obj = null;)
(2)物件賦予了新值,即重新分配了記憶體空間(obj = new Obj();)

     記憶體洩漏:程式中保留著對永遠不再使用的物件的引用。因此這些物件不回被GC回收,卻一直佔用記憶體空間卻毫無用處。即:1)物件是可達的;2)物件是無用的。滿足這兩個條件即可判定為記憶體洩漏。記憶體洩露的原因:1)全域性集合;2)快取;3)ClassLoader。
  應確保不需要的物件不可達,通常採用將物件欄位設定為null的方式,或從容器collection中移除物件。區域性變數不再使用時無需顯式設定為null,因為對區域性變數的引用會隨著方法的退出而自動清除。

 2.6、記憶體調優

   調優目的:減少GC的頻率尤其是Full GC的次數,過多的GC會佔用很多系統資源影響吞吐量。特別要關注Full GC,因為它會對整個堆進行整理。
   主要手段:JVM調優通過配置JVM的引數來提高垃圾回收的速度,合理分配堆記憶體各部分的比例。

 導致Full GC的幾種情況和調優策略:

1     舊生代空間不足
2     調優時儘量讓物件在新生代GC時被回收、讓物件在新生代多存活一段時間和不要建立過大的物件及陣列避免直接在舊生代建立物件
3     持久代(Pemanet Generation)空間不足
4     增大Perm Gen空間,避免太多靜態物件
5     統計得到的GC後晉升到舊生代的平均大小大於舊生代剩餘空間
6     控制好新生代和舊生代的比例
7     System.gc()被顯示呼叫
8     垃圾回收不要手動觸發,儘量依靠JVM自身的機制 

 堆記憶體比例不良設定導致的後果:

12)新生代設定過大
一是新生代設定過大會導致舊生代過小(堆總量一定),從而誘發Full GC;
二是新生代GC耗時大幅度增加;
一般說來新生代佔整個堆1/3比較合適;
34)Survivor設定過大
-XX:MaxTenuringThreshold=n來控制新生代存活時間,儘量讓物件在新生代被回收。

 JVM提供兩種較為簡單的GC策略的設定方式:

1 1)吞吐量優先
2     JVM以吞吐量為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,來達到吞吐量指標。這個值可由-XX:GCTimeRatio=n來設定
3 2)暫停時間優先
4    JVM以暫停時間為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,儘量保證每次GC造成的應用停止時間都在指定的數值範圍內完成。這個值可由-XX:MaxGCPauseRatio=n來設定。

 JVM常見配置:

 1 堆設定
 2         -Xms:初始堆大小
 3         -Xmx:最大堆大小
 4         -XX:NewSize=n:設定年輕代大小
 5         -XX:NewRatio=n:設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代佔整個年輕代年老代和的1/4
 6         -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
 7         -XX:MaxPermSize=n:設定持久代大小
 8 收集器設定
 9         -XX:+UseSerialGC:設定序列收集器
10         -XX:+UseParallelGC:設定並行收集器
11         -XX:+UseParalledlOldGC:設定並行年老代收集器
12         -XX:+UseConcMarkSweepGC:設定併發收集器
13 垃圾回收統計資訊
14         -XX:+PrintGC
15         -XX:+PrintGCDetails
16         -XX:+PrintGCTimeStamps
17         -Xloggc:filename
18 並行收集器設定
19         -XX:ParallelGCThreads=n:設定並行收集器收集時使用的CPU數。並行收集執行緒數。
20         -XX:MaxGCPauseMillis=n:設定並行收集最大暫停時間
21         -XX:GCTimeRatio=n:設定垃圾回收時間佔程式執行時間的百分比。公式為1/(1+n)
22 併發收集器設定
23         -XX:+CMSIncrementalMode:設定為增量模式。適用於單CPU情況。
24         -XX:ParallelGCThreads=n:設定併發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集執行緒數。
View Code

三、總結

    通過對jvm的學習,我們可以深刻地理解到程式的執行原理,以及背後的記憶體和CPU的處理情況,對我們理解多執行緒,高併發,記憶體管理,記憶體優化,程式碼優化等有著重要的作用。

參考文獻:https://www.cnblogs.com/IUbanana/p/7067362.html