1. 程式人生 > >淺談JVM

淺談JVM

一、JVM概述

  JVM (JAVA 虛擬機器),定義了一套編譯,載入,解釋執行JAVA程式碼的規範,

  基於這套規範市場上不同產品實現,例如Hotspot,JRockit,J9等.

  其簡易記憶體體系結構如下:

  

二、堆的記憶體劃分:

  

  Java堆的記憶體劃分如圖所示,分別為年輕代、Old Memory(老年代)、Perm(永久代)。其中在Jdk1.8中,永久代被移除,使用MetaSpace代替。
  1、新生代:
    (1)使用複製清除演算法(Copinng演算法),原因是年輕代每次GC都要回收大部分物件。新生代裡面分成一份較大的Eden空間和兩份較小的Survivor空間。每次只使用Eden和其中一塊Survivor空間,然後垃圾回收的時候,把存活物件放到未使用的Survivor(劃分出from、to)空間中,清空Eden和剛才使用過的Survivor空間。

    (2)分為Eden、Survivor From、Survivor To,比例預設為8:1:1
    (3)記憶體不足時發生Minor GC
  2、老年代:
    (1)採用標記-整理演算法(mark-compact),原因是老年代每次GC只會回收少部分物件。
  3、Perm:用來儲存類的元資料,也就是方法區。
    (1)Perm的廢除:在jdk1.8中,Perm被替換成MetaSpace,MetaSpace存放在本地記憶體中。原因是永久代進場記憶體不夠用,或者發生記憶體洩漏。
    (2)MetaSpace(元空間):元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。

三、GC垃圾回收:

  常見的垃圾回收演算法:  

    1、Mark-Sweep(標記-清除演算法):
      (1)思想:標記清除演算法分為兩個階段,標記階段和清除階段。標記階段任務是標記出所有需要回收的物件,清除階段就是清除被標記物件的空間。
      (2)優缺點:實現簡單,容易產生記憶體碎片
    2、Copying(複製清除演算法):
      (1)思想:將可用記憶體劃分為大小相等的兩塊,每次只使用其中的一塊。當進行垃圾回收的時候了,把其中存活物件全部複製到另外一塊中,然後把已使用的記憶體空間一次清空掉。
      (2)優缺點:不容易產生記憶體碎片;可用記憶體空間少;存活物件多的話,效率低下。

    3、Mark-Compact(標記-整理演算法):
      (1)思想:先標記存活物件,然後把存活物件向一邊移動,然後清理掉端邊界以外的記憶體。
      (2)優缺點:不容易產生記憶體碎片;記憶體利用率高;存活物件多並且分散的時候,移動次數多,效率低下

    4、分代收集演算法:(目前大部分JVM的垃圾收集器所採用的演算法):

    思想:把堆分成新生代和老年代。(永久代指的是方法區)

      (1) 因為新生代每次垃圾回收都要回收大部分物件,所以新生代採用Copying演算法。新生代裡面分成一份較大的Eden空間和兩份較小的Survivor空間。每次只使用Eden和其中一塊Survivor空間,然後垃圾回收的時候,把存活物件放到未使用的Survivor(劃分出from、to)空間中,清空Eden和剛才使用過的Survivor空間。
      (2) 由於老年代每次只回收少量的物件,因此採用mark-compact演算法。
      (3) 在堆區外有一個永久代。對永久代的回收主要是無效的類和常量。

  幾種不同的垃圾回收型別:

    (1)Minor GC:從年輕代(包括Eden、Survivor區)回收記憶體。

    (2)Major GC:清理整個老年代,當eden區記憶體不足時觸發。

    (3)Full GC:清理整個堆空間,包括年輕代和老年代。當老年代記憶體不足時觸發。

四、JVM優化

  1、一般來說,當survivor區不夠大或者佔用量達到50%,就會把一些物件放到老年區。通過設定合理的eden區,survivor區及使用率,可以將年輕物件儲存在年輕代,從而避免full GC,使用-Xmn設定年輕代的大小

  2、對於佔用記憶體比較多的大物件,一般會選擇在老年代分配記憶體。如果在年輕代給大物件分配記憶體,年輕代記憶體不夠了,就要在eden區移動大量物件到老年代,然後這些移動的物件可能很快消亡,因此導致full GC。通過設定引數:-XX:PetenureSizeThreshold=1000000,單位為B,標明物件大小超過1M時,在老年代(tenured)分配記憶體空間。

  3、一般情況下,年輕物件放在eden區,當第一次GC後,如果物件還存活,放到survivor區,此後,每GC一次,年齡增加1,當物件的年齡達到閾值,就被放到tenured老年區。這個閾值可以同構-XX:MaxTenuringThreshold設定。如果想讓物件留在年輕代,可以設定比較大的閾值。

  4、設定最小堆和最大堆:-Xmx-Xms穩定的堆大小堆垃圾回收是有利的,獲得一個穩定的堆大小的方法是設定-Xms和-Xmx的值一樣,即最大堆和最小堆一樣,如果這樣子設定,系統在執行時堆大小理論上是恆定的,穩定的堆空間可以減少GC次數,因此,很多服務端都會將這兩個引數設定為一樣的數值。穩定的堆大小雖然減少GC次數,但是增加每次GC的時間,因為每次GC要把堆的大小維持在一個區間內。

  5、一個不穩定的堆並非毫無用處。在系統不需要使用大記憶體的時候,壓縮堆空間,使得GC每次應對一個較小的堆空間,加快單次GC次數。基於這種考慮,JVM提供兩個引數,用於壓縮和擴充套件堆空間。
    (1)-XX:MinHeapFreeRatio 引數用於設定堆空間的最小空閒比率。預設值是40,當堆空間的空閒記憶體比率小於40,JVM便會擴充套件堆空間
    (2)-XX:MaxHeapFreeRatio 引數用於設定堆空間的最大空閒比率。預設值是70, 當堆空間的空閒記憶體比率大於70,JVM便會壓縮堆空間。
    (3)當-Xmx和-Xmx相等時,上面兩個引數無效

  6、通過增大吞吐量提高系統性能,可以通過設定並行垃圾回收收集器。
    (1)-XX:+UseParallelGC:年輕代使用並行垃圾回收收集器。這是一個關注吞吐量的收集器,可以儘可能的減少垃圾回收時間。
    (2)-XX:+UseParallelOldGC:設定老年代使用並行垃圾回收收集器。

  7、嘗試使用大的記憶體分頁:使用大的記憶體分頁增加CPU的記憶體定址能力,從而系統的效能。-XX:+LargePageSizeInBytes 設定記憶體頁的大小

  8、使用非佔用的垃圾收集器。-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停頓。

  9、-XXSurvivorRatio=3,表示年輕代中的分配比率:survivor:eden = 2:3

  10、JVM效能調優的工具:
    (1)jps(Java Process Status):輸出JVM中執行的程序狀態資訊(現在一般使用jconsole)
    (2)jstack:檢視java程序內執行緒的堆疊資訊。
    (3)jmap:用於生成堆轉存快照
    (4)jhat:用於分析jmap生成的堆轉存快照(一般不推薦使用,而是使用Ecplise Memory Analyzer)
    (3)jstat:是JVM統計監測工具。可以用來顯示垃圾回收資訊、類載入資訊、新生代統計資訊等。
    (4)VisualVM:故障處理工具

五、類載入機制

  1、可以在elipse類中右鍵Run configurations-->Arguments-->VM Arguments中設定引數:

    -XX:+TraceClassLoading                ----檢視類的載入順序

    -XX:MetaspaceSize=2m                 ----設定metaspace的大小

     -XX:MaxMetaspaceSize=10m              ----設定metaspace大小的最大值

  2、每一個類的最先載入的三個類

    

 

  3、類載入器把class檔案中的二進位制資料讀入到記憶體中,存放在方法區,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類載入的步驟如下:
    載入:查詢並載入類的二進位制資料(把class檔案裡面的資訊載入到記憶體裡面)
    連線:把記憶體中類的二進位制資料合併到虛擬機器的執行時環境中
     (1)驗證:確保被載入的類的正確性。包括:

         A、類檔案的結構檢查:檢查是否滿足Java類檔案的固定格式
         B、語義檢查:確保類本身符合Java的語法規範
         C、位元組碼驗證:確保位元組碼流可以被Java虛擬機器安全的執行。位元組碼流是操作碼組成的序列。每一個操作碼後面都會跟著一個或者多個運算元。位元組碼檢查這個步驟會檢查每一個操作碼是否合法。
         D、二進位制相容性驗證:確保相互引用的類之間是協調一致的。

     (2)準備:為類的靜態變數分配記憶體,並將其初始化為預設值
     (3)解析:把類中的符號引用轉化為直接引用(比如說方法的符號引用,是有方法名和相關描述符組成,在解析階段,JVM把符號引用替換成一個指標,這個指標就是直接引用,它指向該類的該方法在方法區中的記憶體位置)
    初始化:為類的靜態變數賦予正確的初始值。當靜態變數的等號右邊的值是一個常量表達式時,不會呼叫static程式碼塊進行初始化。只有等號右邊的值是一個執行時運算出來的值,才會呼叫static初始化。

  4、常見的三種類載入器:

    AppClassLoader--應用類載入器,負責載入我們自己寫的類

    ExtClassLoader--擴充套件類載入器,負責載入擴充套件包(jre\lib\ext\*.jar)

    BootstrapClassLoader--根類載入器,負責載入核心包(jre\lib\rt.jar)

  5、類載入類的兩種方式----顯式載入和隱式載入

    (1)顯式載入

 1 class classA{
 2     //類載入時可以執行靜態程式碼塊,但不一定會執行
 3     static {
 4         System.out.println(11);
 5     }
 6 }
 7 
 8 //不會執行靜態程式碼塊
 9 loader.loadClass("cn.shizhe.ClassLoader.classA");
10 //會執行靜態程式碼塊
11 Class.forName("cn.shizhe.ClassLoader.classA",true,loader);
12 //不會執行靜態程式碼塊
13 Class.forName("cn.shizhe.ClassLoader.classA",false,loader);

    (2)隱式載入的時機:--預設都會進行初始化

      訪問類的靜態屬性時?(分情況)

      訪問static final修飾的八種基本資料型別和字串時不會觸發類的載入
      
訪問static 修飾的任意屬性時都會觸發類的載入
      訪問類的靜態方法
      構建類的物件時

  6、類的被動載入與主動載入

 1 class class1{
 2     static int a = 100;
 3     static {
 4         System.out.println("class1.static");
 5     }
 6 }
 7 
 8 class class2 extends class1{
 9     static {
10         System.out.println("class2.static");
11     }
12 }
13 public class TestClassObject08 {
14     public static void main(String[] args) {
15         //class1為主動載入,class2為被動載入(不執行static初始化操作)
16         System.out.println(class2.a);
17     }
18 }

&n