JVM詳解


Java虛擬機(JVM)

JVM是什么?

JVM是一種用於計算設備的規范,它是一個虛擬出來的計算機,是通過在實際的計算機上仿真模擬計算機的各個功能來實現的。Java語言的一個非常重要的特點就是與平台的無關性。而使用Java虛擬機是實現這一特點的關鍵。每個Java虛擬機都着一個清晰的任務:執行Java程序。程序執行時虛擬機才啟動,程序結束時它才停止。

JVM的生命周期?

  1. 啟動。啟動一個Java程序的時候,就產生了一個JVM實例。
  2. 運行。main方法是程序的入口,任何其他線程均有它啟動。
  3. 消亡。當程序中所有非守護線程都終止時,JVM才退出。若安全管理器允許,程序也可以使用Runtime類或者System.exit()來退出程序。

Java中的線程

Java中的線程分為兩種:守護線程(daemon)和普通線程(non-daemon)。守護線程是JVM自己使用的的線程,如GC。當然也可以自己設置守護線程,但包含main方法的初始線程不是守護線程。
只要JVM中還有普通線程在執行,JVM就不會停止。但如果有足夠的權限,可以調用exit方法終止程序。

JVM的體系結構

  1. 類裝載器(ClassLoader,用來裝載.class文件)
  2. 執行引擎(執行字節碼或本地方法)
  3. 運行時數據區(方法區、堆區、虛擬機棧、本地方法棧、程序計數器)

JVM類的加載機制

Java程序並不一個可執行文件,是由多個獨立的類文件組成。這些類文件並非一次性全部裝入內存,而是依據程序逐步載入

JVM的類加載是通過ClassLoader及其子類類完成的,類的層次關系和記載順序可以由下圖說明:

  1. Bootstrap ClassLoader
  • JVM的根ClassLoader,使用C++語言實現
  • 使用它加載Java的核心API:$JAVA_HOME/jre/lib/rt.jar,這個jar中包含了java規范定義的所有接口以及實現。
  • JVM啟動時即初始化此ClassLoader
  1. Extension ClassLoader
  • 加載Java的擴展API(lib/ext中的類)
  1. App ClassLoader
  • 加載classpath目錄下的class
  1. Custom ClassLoader
  • 屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據J2EE規范自行實現ClassLoader

類在加載之前會先被檢查是否已被加載,檢查下的順序是自底向上,從Custom ClassLoader到Bootstrap ClassLoader逐層檢查,只要某個ClassLoader檢查到已加載就視為此類已加載。從而保證類只加載一次。 而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。

雙親委派機制

JVM在加載類時默認采用的是雙親委派機制。
通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸。如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。

為什么使用這種機制

  1. 避免了類被重復加載
  2. 更安全。如果有人想替換系統級別的類:String.java。篡改它的實現,在這種機制下這些系統的類已經被Bootstrap classLoader加載過了。(為什么?因為當一個類需要加載的時候,最先去嘗試加載的就是BootstrapClassLoader。)所以其他類加載器並沒有機會再去加載,從一定程度上防止了危險代碼的植入。

破壞雙親委派機制

雙親委派機制並不是一種強制性的約束模型,而是Java設計者推薦給開發者的類加載器實現方式。

線程上下文類加載器,這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那么這個類加載器就是應用程序類加載器。像JDBC就是采用了這種方式。這種行為就是逆向使用了加載器,違背了雙親委派模型的一般性原則。

類加載機制總結

JVM運行時數據區

堆區(Heap)

  1. 被所有線程共享的一塊內存區域,在虛擬機啟動時創建
  2. 用來存儲對象實例
  3. 可以通過-Xms和-Xmx控制堆空間的大小
  4. OutOfMemory: 當堆中沒有完成實例分配,且堆再也無法擴展時。

JVM堆是垃圾收集器管理的主要區域。
堆空間還可細分為:

  • 新生代(Eden/Young)[s0、s1]
  • 年老代(Tenured/Old)
  • 持久代(Permanent),在方法區,不屬於堆

新生代: 新建的對象都有新生代分配內存。新生代常常又被划分為Eden和Survivor區,Eden空間不足時會把存活的對象轉移到Survivor。新生代的大小可以使用-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。

年老代: 存放經過多次垃圾回收依然存活的對象

持久代: 存放靜態文件,如今Java類、方法等。持久代在方法區,對垃圾回收沒有顯著影響。

方法區(Meta Space)

  • 線程間共享

  • 用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯后的代碼等數據等

  • OutOfMemoryError:當方法區無法滿足內存分配時

  • 運行時常量池

    • 方法區的一部分
    • 用於編譯期生成的各種字面量與符號引用,如String類型的引用就存放在字符串常量池
    • OutOfMemoryError:當常量池無法在申請到內存時

Java虛擬機棧(VM Stack)

  • 線程私有,生命周期與線程相同
  • 存儲方法的局部變量表(基本類型、對象引用)、操作數棧、動態鏈接、方法出口等信息
  • Java方法執行的內存模型,每個方法執行的同時都會創建一個棧幀,每個方法被調用直至完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
  • StackOverflowError:當線程請求的棧深度大於虛擬機所允許的深度
  • OutOFMemoryError:如果棧的擴展時無法申請到足夠的內存

JVM棧是線程私有的,每個線程創建的同時都會創建JVM棧,JVM棧中存放的為當前線程中局部基本類型的變量、部分的返回結果以及Stack Frame。其他引用類型的對象在JVM棧上僅存放變量名指向堆上對象實例的首地址。

本地方法棧(Native Method Stack)

  • 與虛擬機棧相似,主要是為虛擬機使用到的本地方法服務,在HotSpot虛擬機中和虛擬機棧合二為一

程序計數器(Programma Counter Register)

  • 當前線程所執行的字節碼的行號指示器
  • 當前線程私有
  • 不會出現OutOfMemoryError

直接內存(Direct Memory)

  • 直接內存並不是虛擬機運行的一部分,也不是Java虛擬機規范中定義的內存區域,但是這部分內存也被頻繁使用
  • NIO可以使用Native函數庫直接分配堆外內存,堆中的DirectByteBuffer對象作為這塊內存的引用進行操作
  • 大小不受Java堆大小的限制,受本機(服務器)內存限制
  • OutOfMemoryError異常:系統內存不足時

總結:

  • Java對象實例存放在堆中,常量存放在方法區的常量池;
  • 虛擬機加載的類信息、常量、靜態變量、即時編譯后的代碼等數據存放在方法區;
  • 以上區域是線程共享的
  • 棧是線程私有的,存放該方法的局部變量表(基本類型、對象引用)、操作數棧、動態鏈接、方法出口等信息。
  • 一個Java程序對應着一個JVM實例,一個方法(線程)對應一個虛擬機棧

Java代碼的編譯和執行過程

Java代碼的編譯和執行包括了三個重要機制:
(1)Java源碼編譯機制(.java -> .class)
(2)類加載機制(ClassLoader)
(3)類執行機制(執行引擎)

Java源碼編譯機制

Java源代碼是不能被機器識別的,需要先經過編譯器編譯成JVM可以執行的.class字節碼文件,再由解釋器解釋運行。
即:Java源文件(.java) -- Java編譯器 --> Java字節碼文件 (.class) -- Java解釋器 --> 執行。

  • 字節碼文件(.class)是平台無關的

  • Java中字符只以一種形式存在:Unicode。字符轉換發生在JVM和OS交界處(Reader/Writer)

  • 最后生成的class文件由以下部分組成:

    • 結構信息。包括class文件格式版本號及各部分的數量與大小的信息
    • 元數據。對應於Java源碼中聲明與常量的信息。包含類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池
    • 方法信息。對應Java源碼中語句和表達式對應的信息。包含字節碼、異常處理器表、求值棧與局部變量區大小、求值棧的類型記錄、調試符號信息

類加載機制在上面已經提到

類執行機制

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

JVM是基於棧的體系結構來執行class字節碼的。
線程創建后,都會產生程序計數器(PC)和棧(Stack),程序計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每個棧幀對應着每個方法的每次調用,而棧幀又是有局部變量區和操作數棧兩部分組成,局部變量區用於存放方法中的局部變量和參數,操作數棧中用於存放方法執行過程中產生的中間結果。

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

  • 解釋屬於第一代JVM
  • 即時編譯JIT屬於第二代JVM
  • 自適應優化(目前Sun的HotspotJVM采用這種技術)則吸取第一代JVM和第二代JVM的經驗,采用兩者結合的方式

開始對所有的代碼都采取解釋執行的方式,並監視代碼執行情況。對那些經常調用的方法啟動一個后台線程,將其編譯為本地代碼,並進行優化。若方法不再頻繁使用,則取消編譯過的代碼,仍對其進行解釋執行。

JVM垃圾回收

GC的基本原理:將內存中不再被引用的對象進行回收,GC中用於回收的方法稱為收集器。垃圾:不再被引用的對象。

由於GC需要消耗一些資源和時間,Java在對對象的生命周期特征進行分析后,按照新生代、舊生代的方式來對對象進行收集,以盡可能的縮短GC對應用造成的暫停。

  • 對新生代的對象的收集稱為minor GC
  • 對舊生代的對象的收集稱為Full GC
  • 程序中主動調用System.gc()的GC為Full GC

 Java垃圾回收是單獨的后台線程gc執行的,自動運行無需顯示調用。即使主動調用了java.lang.System.gc(),該方法也只會提醒系統進行垃圾回收,但系統不一定會回應,可能會不予理睬。

判斷一塊內存空間是否符合回收標准:

  • 對象引用賦予了空值,且之后再未調用(obj = null;)
  • 對象引用賦予了新值,即重新分配了內存空間(obj = new Obj();)

內存泄漏: 程序中保留着對永遠不再使用的對象的引用。因此這些對象不回被GC回收,卻一直占用內存空間卻毫無用處。
即:1)對象是可達的;2)對象是無用的。滿足這兩個條件即可判定為內存泄漏。

  應確保不需要的對象不可達,通常采用將對象字段設置為null的方式,或從容器collection中移除對象。局部變量不再使用時無需顯示設置為null,因為對局部變量的引用會隨着方法的退出而自動清除。

內存泄露的原因: 1)全局集合;2)緩存;3)ClassLoader

JVM內存調優

調優目的: 減少GC的頻率尤其是Full GC的次數,過多的GC會占用很多系統資源影響吞吐量。特別要關注Full GC,因為它會對整個堆進行整理。

主要手段: JVM調優主要通過配置JVM的參數來提高垃圾回收的速度,合理分配堆內存各部分的比例。

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

  • 舊生代空間不足
  • 調優時盡量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象
  • 持久代(Pemanet Generation)空間不足
    增大Perm Gen空間,避免太多靜態對象
  • 統計得到的GC后晉升到舊生代的平均大小大於舊生代剩余空間控制好新生代和舊生代的比例
  • System.gc()被顯示調用
    垃圾回收不要手動觸發,盡量依靠JVM自身的機制

堆內存比例不良設置會導致什么后果:

1)新生代設置過小

一是新生代GC次數非常頻繁,增大系統消耗;二是導致大對象直接進入舊生代,占據了舊生代剩余空間,誘發Full GC

2)新生代設置過大

一是新生代設置過大會導致舊生代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加

一般說來新生代占整個堆1/3比較合適

3)Survivor設置過小

導致對象從eden直接到達舊生代,降低了在新生代的存活時間

4)Survivor設置過大

導致eden過小,增加了GC頻率

另外,通過-XX:MaxTenuringThreshold=n來控制新生代存活時間,盡量讓對象在新生代被回收

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

1)吞吐量優先

JVM以吞吐量為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,來達到吞吐量指標。這個值可由-XX:GCTimeRatio=n來設置

2)暫停時間優先

JVM以暫停時間為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,盡量保證每次GC造成的應用停止時間都在指定的數值范圍內完成。這個值可由-XX:MaxGCPauseRatio=n來設置

JVM常見配置

  1. 堆設置
    -Xms:初始堆大小
    -Xmx:最大堆大小
    -XX:NewSize=n:設置年輕代大小
    -XX:NewRatio=n:設置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
    -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
    -XX:MaxPermSize=n:設置持久代大小
  2. 收集器設置
    -XX:+UseSerialGC:設置串行收集器
    -XX:+UseParallelGC:設置並行收集器
    -XX:+UseParalledlOldGC:設置並行年老代收集器
    -XX:+UseConcMarkSweepGC:設置並發收集器
  3. 垃圾回收統計信息
    -XX:+PrintGC
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps
    -Xloggc:filename
  4. 並行收集器設置
    -XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。5. 並行收集線程數。
    -XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
    -XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)
  5. 並發收集器設置
    -XX:+CMSIncrementalMode:設置為增量模式。適用於單CPU情況。
    -XX:ParallelGCThreads=n:設置並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。

JVM常見問題

  1. 如何理解JVM?(一種JAVA虛擬機規范,基於此規范有不同JVM產品)
    答:JVM是一種用於計算設備的規范,它是一個虛擬出來的計算機,是通過在實際的計算機上仿真模擬計算機的各個功能來實現的。Java語言的一個非常重要的特點就是與平台的無關性。而使用Java虛擬機是實現這一特點的關鍵。每個Java虛擬機都着一個清晰的任務:執行Java程序。程序執行時虛擬機才啟動,程序結束時它才停止。

  2. 你了解有哪些JVM產品?
    答:HotSpot,Sun JDK和OpenJDK中所帶的虛擬機,也是目前使用范圍最廣的虛擬機。
    HotSpot VM的熱點代碼探測能力可以通過執行計數器找出最具有編譯價值的代碼,然后通知JIT編譯器以方法為單位進行編譯。

  3. 哪些程序代碼會被編譯為本地代碼?如何編譯為本地代碼?
    答:程序中的代碼只有是熱點代碼時,才會編譯為本地代碼
    熱點代碼:(1)被多次調用的方法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM