深刻理解Java虛擬機

JVM虛擬機

  • java技術體系java

    • 程序設計語言<----->編譯器<----->class文件<----->虛擬機算法

      • class文件格式規範給定,能夠本身設計語言,本身編寫編譯器,生成相同的class文件便可
      • 只要class文件的規範給定,不一樣的編程語言根據不一樣的編譯器進行編譯,生成符合規範的class文件,就能運行在JVM虛擬機上

一. 內存結構

1.1 運行時數據區域

image.png

  1. 程序計數器數據庫

    • 程序計數器是一塊較小的內存空間,它能夠看作是當前線程所執行的字節碼的行號指示器,字節碼解釋器工做就是經過改變這個計數器的值來選取下一條須要執行指令的字節碼指令
    • 程序計算器處於線程獨佔區
    • 若是線程執行的是java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址,若是正在執行的是native方法,這個計數器的值爲undefine
    • 此區域是惟一一個在java虛擬機規範沒有規定任何OutOfMemoryError狀況的區域
  2. 虛擬機棧編程

    • 虛擬機棧描述的是Java方法執行的動態內存模型
    • 棧幀json

      • 每一個方法執行,都會建立一個棧幀,伴隨着方法從建立到執行結束.用於存儲局部變量表,操做數棧,動態連接,方法出口等.每個方法從調用直到執行結束,就對應着一個棧幀從虛擬機棧中入棧到出棧的過程
    • 局部變量表數組

      • 存放編譯器可知的各類基本數據類型,引用類型,returnAddress類型
      • 局部變量表的內存空間在編譯期完成分配,當進入一個方法時,這個方法須要在幀分配多少內存是固定的,在方法運行期間是不會改變局部變量表的大小
  3. 本地方法棧緩存

    • 虛擬機棧爲虛擬機執行java方法服務
    • 本地方法棧爲虛擬機執行native方法服務
  4. Java堆安全

    • JVM鎖管理的內存中最大的一塊,線程共享,主要存放對象和數組
    • 內部會劃分出多個線程私有的分配緩衝區TLAB(Thread Local Allocation Buffer),能夠位於物理上不連續的空間,可是邏輯上要連續
    • 垃圾收集器管理的主要區域
    • 新生代,老年代,Eden
  5. 方法區服務器

    • 屬於共享內存區域,存儲虛擬機加載的類信息,常量,靜態變量,便是編輯器編譯後的代碼等數據網絡

      • 魔數
      • 類的版本
      • 字段
      • 方法
      • 接口
      • 運行時常量池

        • 屬於方法區的一部分,用於存放編譯器生成的各類字面量和符號引用
        • 編譯器和運行期(String的intern())均可以將常量放入池中
  6. 直接內存

    • 非虛擬機運行時數據區的部分
    • 在JDK1.4中新加入NIO,引入一種基於管道(Channel)和緩衝(Buffer)的I/O方式,它可使用Native函數直接分配對外內存,而後用過一個存儲在Java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做,能夠避免在Java堆和Native堆中來回的數據耗時操做

image.png

  • 附:

    • 堆體系結構

      • 一個JVM實例只存在一個堆內存,堆內存的大小是能夠調節的
      • 類加載器讀取了類文件後,須要把類,方法,常變量放到堆內存中,保存全部引用類型的真實信息,以便執行器執行
      • 堆在邏輯上分爲三部分

        • 新生代(Young Generation,稱YoungGen),位於堆空間

          • 新生代又分爲Eden區和Survior區

            • Eden:新建立的對象
            • Survior:通過垃圾回收,可是垃圾回收次數小於15次的對象
        • 老年代(Old Generation,稱OldGen,TenuringGen),位於堆空間

          • 垃圾回收次數超過15次,且依然存活的對象
        • 永久代(Permanent Geneartion,稱PermGen),位於非堆空間---方法區

          • 永久代是一個常駐內存區域,用於存放JDK自身所攜帶的class,interface的元數據,也就是說它存儲的是運行環境必須的類信息,被裝進此區域的數據是不會被垃圾回收器回收的,關閉JVM纔會釋放此區域所佔的內存
* java8刪除了堆中的永久代,增長了元空間

  * java7以前hotspot虛擬機對方法區的實現爲永久代(PermGen),永久代和堆相互隔離,永久代的大小在啓動JVM時能夠設定一個固定值
  * java7中,字符串變量從永久代移到堆中
  * java8中,移除了永久代,元空間(Metaspace)做爲方法區的實現,永久代與堆不相連,但與堆物理共享內存,邏輯上能夠認爲在堆中,元空間在本地內存
  • 堆的生命週期

    • 新生代

      • 新生代是對象的誕生,成長,消亡的區域,一個對象在這裏產生,應用,最後被垃圾回收器收集,銷燬
      • 新生代分爲兩部分:Eden Space和Survior Space,全部對象都是在Eden區被new出來
      • Survior區有兩個:0區(Survior 0 space)和1區(Survior 1 space),當Eden區的空間用完時,程序又須要建立新對象時,JVM的垃圾回收器將對Eden進行垃圾回收Minor GC
      • 將Eden中再也不被其餘對象所引用的對象進行銷燬,而後將Enden中剩餘對象移動到Survior 0區,當Survior 0區滿了,再將該區進行垃圾回收,而後移動到Survior 1區,如此反覆,達到必定年齡的對象將移入老年代
    • 老年代

      • 若老年代也滿了,這個時候會產生Major GC(Full GC),進行老年代的內存清理,若老年代執行Full GC以後發現仍然沒法進行對象的保存,就會產生OOM

        • 若出現java.lang.OutOfMemoryError:Java head space異常,說明Java虛擬機的堆內存不夠

          • Java虛擬機的堆內存不夠,能夠經過參數-Xms(初始堆內存),-Xmx(最大堆內存)來調整
          • 代碼中建立了大量大對象,而且超時間不能被垃圾收集器清理(存在被引用)
    • 元空間

      • 元空間的本質和永久代相似,都是對JVM規範中方法區的實現,不過元空間和永久代最大的區別在於:元空間並不在虛擬機中,而是使用本地內存.所以,默認狀況下,元空間僅受本地內存限制,可是能夠經過如下參數來指定元空間的大小

        • -XX:MetaspaceSize:初始空間大小,達到該值會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當下降該值.若是釋放不多的空間,那麼在不超過MaxMetaspaceSize的狀況下,適當提升該值
        • -XX:MaxMetaspaceSize:最大空間,默認無限制
  • 常量池

    • 方法區存着類的信息,常量,靜態變量,即類被編譯後的數據.具體來講,方法區存放着類的版本,字段,方法,接口,常量池

      image.png

      • 常量池

        • 字面量

          • 文本字符串
          • 被聲明爲Final的常量值
          • 基本數據類型的值
          • 其餘
        • 符號引用

          • 類和結構的徹底限定名
          • 字段名稱和描述符
          • 方法名稱和描述符
      • class文件信息

        image.png

        • class文件中存儲的數據類型

          image.png

        • 反編譯class文件

          image.png

    • 靜態常量池(class文件常量池)和動態常量池的區別

      • 靜態常量池儲存的是當class文件被java虛擬機加載進來後存儲在方法區的一些字面量和符號引用,字面量包括字符串,基本類型的常量,符號引用其實引用的就是常量池裏面的內容,但符號引用不是直接存儲字符串,而是存儲字符串在常量池中的索引

        • 位於class文件中,javap -verbose反編譯展現的字節碼即爲靜態常量池
      • 動態常量池式當class文件被加載完成後,java虛擬機將靜態常量池裏的內容轉移到動態常量池裏,在靜態常量池的符號引用有一部分是會被轉變爲直接引用的,好比類的靜態方法或私有方法,實例構造方法,父類方法,這是由於這些方法不能被重寫成其餘版本,因此能在加載的時候能夠將符號引用轉變爲直接引用,而其餘的一些方法則須要在這個方法被第一次調用時纔將符號引用轉變爲直接引用

        • 在class文件被加載進內存後,常量池保存在了方法區中,一般說的常量池爲運行時常量池
      • 總結

        • 方法區裏存儲着class文件的信息和動態常量池,class文件的信息包括類信息和靜態常量池.能夠將類的信息是對class內容的一個框架,裏面具體的內容能夠經過常量池來存儲
        • 動態常量池裏的內容除了是靜態常量池裏的內容外,還將靜態常量池裏的符號引用轉變爲直接引用,並且動態常量池裏的內容是能動態添加的---如String#intern方法

          • 這裏String常量池是包含在動態常量池中的,但在jdk1.8後將String常量池放到了堆中
> 1. 字符串常量池
>
>    ```java
>    String a = "abc";
>    String b = new String("abc");
>    System.out.println(a == b); //false
>    
>    //對象b存儲在堆中,a做爲字面量一開始存儲在了class文件中,以後運行期,轉存至方法區中,因此二者比較爲false
>    ```
>
>    ```java
>        String s1 = "Hello";
>        String s2 = "Hello";
>        String s3 = new String("Hello");
>        String s4 = "Hel" + "lo";
>        String s5 = "Hel" + new String("lo");
>        String s6 = s5.intern();
>        String s7 = "H";
>        String s8 = "ello";
>        String s9 = s7 + s8;
>    
>    //s1和s2都指向了方法區常量池中的Hello
>        System.out.println(s1 == s2);  // true
>    //一個在常量池,一個在堆中
>        System.out.println(s1 == s3);  // false
>    //由於作+號運算,若常量池有結果字符串,則返回
>        System.out.println(s1 == s4);  // true
>    //在+號運算時進行動態調用,最後仍然是一個String對象存在堆中
>        System.out.println(s1 == s5);  // false
>    //intern()方法:首先在常量池中查找是否存在一份相同的字符串,若是有就返回該字符串的引用,不然就加入到字符串常量池中,動態常量池是能夠改變的
>        System.out.println(s1 == s6);  // true
>    //Java9,由於是動態調用,因此返回的是一個新的String對象
>        System.out.println(s1 == s9);  // false
>    
>    ```
>
>    ```java
>    1.常量拼接
>    
>    public static final String a = "123";
>    public static final String b = "456";
>    
>    public static void main(String[] args){
>        String c = "123456";
>        String d = a+b;
>        System.out.println(c == d);//true
>    }
>    ----反編譯結果----
>        0:ldc        #2        //String 123456
>        2:astore_1
>        3:ldc        #2        //String 123456
>        5:astore_2
>        6:getstatic    #4
>    //對於final類型的常量,他們已經在編譯中就肯定下來,自動執行了+號,把他們拼接起來因此至關於"123456"
>    
>    
>    2.static靜態代碼塊
>    
>    public static final String a;
>    public static final String b;
>    
>    static{
>        a = "123";
>        b = "456";
>    }
>    
>    
>    public static void main(String[] args){
>        String c = "123456";
>        String d = a + b;
>        System.out.println(c == d);
>    }
>    ------反編譯結果-----
>        3: getstatic    #3    //Field a:Ljava/lang/String
>        6: getstatic    #4    //Field b:Ljava/lang/String
>        9: invokeddynamic    #5    //invokeDynamic #0:makeConcatWithConstants
>        
>    //上一個例子是在編譯期間,就已經肯定a和b,而這段代碼中,編譯期static不執行的,a和b的值是位置的,static代碼塊在初始化的時候被執行,而初始化屬於類加載的一部分,屬於運行期,在反編譯的指令中使用了indy指令,動態調用返回String類型對象,位於堆區而不是常量池中
>    
>    
>    3.建立了幾個對象
>    String s1 = new String("xyz");
>    1. 類加載對一個類只會進行一次,"xyz"在類加載時就已經建立並駐留(intern)了(若是該類被加載以前已經有"xyz"字符串被駐留過則不須要重複建立用於駐留的"xyz"實例),駐留的字符串是存放在全局共享的字符串常量池中
>    2. 這段代碼在後續被運行的時候,"xyz"字面量對應的String實例已經固定,不會再被重複建立,因此這段代碼將常量池中的對象複製一份放到heap中,並把heap中的這個對象引用交給s1持有
>    3.這條語句建立了兩個實例對象,一個是全局共享的字符串常量池中的實例,一個new String建立的引用實例
>    
>    
>    String s2 = "a" + "b" + "c";
>    1. 實際上在編譯期,已經將這三個字面量合成了一個,這樣其實是一種優化,避免建立了多餘字符串對象,也沒有字符串拼接問題
>    2. 只建立一個對象,在常量池中只保存一個引用
>    ```
>
> 2. 包裝類的常量池技術
>
>    * jdk1.5中引入了自動拆箱和自動裝箱機制,自動裝箱常見的是valueOf這個方法,自動拆箱就是intValue方法,除了包裝類Long和Double沒有實現這個緩存技術,其餘的包裝類都實現了它
>
>      ```java
>      public static Integer valueOf(int i){
>          if(i >= IntegerCache.low && i <= IntegerCache.high){
>              return IntegerCache.cache[i + (-IntegerCache.low)];
>          }
>          return new Integer(i);
>      }
>      
>      
>      private static class IntegerCache{
>          static final int low = -128;
>          static final int high;
>          static final Integer cache[];
>          
>          static{
>              //high value may be configured by property
>              int h = 127;
>              String integerCacheHighPropValue = VM
>                  .getSaveProperty("java.lang.Integer.IntegerCache.high");
>              if(IntegerCacheHighPropValue != null){
>                  try{
>                      int i = parseInt(integerCacheHighPropValue);
>                      i = Math.max(i,127);
>                      //Maxmun array size is Integer.MAX_VALUE
>                      h = Math.min(i,Integer.MAX_VALUE - (-low) -1);
>                  }catch(NumberFormatException nfe){
>                  //if the property cannot be parsed into an int,ignore it
>                  }
>              }
>              high = h;
>              
>              cache = new Integer[(high - low) + 1];
>              int j = low;
>              for(int k = 0;k < cache.length;k++){
>                  cache[k] = new Integer(j++);
>              }
>              
>              //range[-128,127]must be interned
>                  assert IntegerCache.high >= 127;
>          }
>          
>          private IntegerCache(){}
>      }
>      ```
>
>    * 能夠看到從-128~127的數所有自動加入到了常量池裏面,覺得這個段的數使用的常量值的地址都是同樣的
>
>      ```java
>      Integer a = 40;
>      Integer b = 40;
>      Double c = 40.0;
>      Double d = 40.0;
>      
>      System.out.print(a == b);//true
>      System.out.print(c == d);//false
>      
>      /*
>      1.==這運算在不出現算數運算符的狀況下,不會自動拆箱,因此a和b他們不是數值進行比較,仍然是比較地址是否指向同一地址內存
>      2.他們都在常量池存儲
>      3.編譯階段已經將代碼轉成了調用valueOf方法,使用的是常量池,若是超過範圍則建立新的對象
>      */
>      
>      Integer a1 = 40;
>      Integer a2 = 40;
>      Integer a3 = 0;
>      Integer a4 = new Integer(40);
>      Integer a5 = new Integer(40);
>      Integer a6 = new Integer(0);
>      Integer b1 = 400;
>      Integer b2 = 400;
>      
>      System.out.println(a1 == a2);//true
>      System.out.println(a1 == a2 + a3);//true
>      System.out.println(a1 == a4);//false
>      System.out.println(a4 == a5);//false
>      System.out.println(a4 == a5 + a6);//true
>      System.out.println(400 == a5 + a6);//true
>      System.out.println(b1 == b2);//false
>      
>      /*
>      1.當出現運算時,Integer不能直接用來運算,因此會進行一次自動拆箱爲基本數據進行比較
>      2.==這個符號,既能夠比較基本類型,也能夠比較內存地址,當進行運算時比較數據大小,不然比較內存地址是否相同
>      3.a1,a2是指向常量池中同一個地址,a3也是位於常量池中
>      4.a4,a5,a6位於堆中,並各自指向不一樣的對象
>      5.b1,b2超過範圍,都是堆中的新對象
>      */
>      ```

1.2 虛擬機對象

1.2.1 對象的建立

  1. 遇到new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用是否已經被加載,解析和初始化過,若是沒有,執行相應的類加載
  2. 類加載檢查經過以後,爲新對象分配內存(內存大小在類加載完成後便可確認),在堆的空閒內存中劃分一塊區域

    1. 指針碰撞-內存規整
    2. 空閒列表-內存交錯
  3. 每一個線程在堆中都會有私有的分配緩衝區TLAB,這樣能夠很大程度避免在併發狀況下頻繁建立對象形成的線程不安全
  4. 內存空間分配完成後會初始化爲0(不包括對象頭),接下來就是填充對象頭,把對象是哪一個類的實例,如何才能找到類的元數據信息,對象的哈希碼,對象的GC分代年齡等信息存入對象頭
  5. 執行new指令後執行init方法後纔算一份真正可用的對象建立成功

1.2.2 對象的內存佈局

  • 子HotSpot虛擬機中,分爲三塊區域:對象頭(Header),實例數據(Instance Data)和對齊填充(Padding)

    1. 對象頭:

      1. 自身運行時數據(Mark Word)

        • 哈希碼,GC分代年齡,鎖狀態標誌,線程持有的鎖,偏向線程ID,偏向時間戳等
      2. 類型指針

        • 即對象指向它的類的元數據指針,虛擬機經過這個指針肯定這個對象是哪一個類的實例
    2. 實例數據:

      • 程序代碼中所定義的各種數據的字段內容(包括父類繼承下來的和子類中定義的)
      • 函數的局部變量在堆棧上,類成員變量在堆上,還有常數在常量池

        public class Test{
            public static A a2;
        //方法逃逸
            public A run1(){
                A a1=new A();
                return a1;
            }
        //線程逃逸
            public void run2(A a2){
                this.a2=a2;
            }
        //無逃逸
            public void run3(){
                A a3=new A();
            }
        }
        
        對象引用a1在方法run1中被定義並返回,此爲方法逃逸,該對象分配到堆
        
        類變量a2在方法run2被賦值,由於該類變量可被其餘線程訪問,此爲線程逃逸,該對象分配到堆
        
        對象引用a3在方法run3中被定義,且爲無逃逸,該對象能夠被分配到棧
    3. 對齊填充:

      • 不是必然須要,主要是佔位,保證對象大小是某個字節的整數倍
  • 對象的內存分配

    • 棧上分配

      • VM在Server模式下的逃逸分析能夠分析出某個對象是否永遠只在某個方法,線程範圍內,並無"逃逸"出這個範圍,逃逸分析的一個結果就是對於某些未逃逸對象能夠直接在棧上分配,因爲該對象必定是局部的,因此棧上分配不會有問題.
      • 在實際的應用程序,尤爲是大型程序中反而發現實施逃逸分析可能出現效果不穩定,或因分析過程耗時但沒法有效判別出非逃逸對象而致使性能(即時編譯的收益)有所降低,因此很長一段時間裏沒及時Server Compiler,也默認不開啓逃逸分析
    • TLAB分配

      • 一種是對分配內存空間的動做進行同步處理----實際上虛擬機採用CAS和失敗重試的方式保證更新操做的原子性
      • 另外一種是把內存分配的動做按照線程劃分在不一樣的空間之中記性,即每一個線程在Java堆中預先分配一塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer TLAB)
    • 堆分配

      • 咱們知道堆是由全部線程共享的,既然如此那它就是競爭資源,對於競爭資源,必須採起必要的同步,因此當使用new關鍵字在堆上分配對象時,是須要鎖的.既然有鎖,那一定存在鎖帶來的開銷,並且因爲是對整個堆加鎖,相對而言鎖的粒度仍是比較大的,影響效率,而不管是TLAB仍是棧都是線程私有的,私有即避免了競爭

1.2.3 對象的訪問定位

  • 使用對象時,經過棧上的reference數據來操做堆上的具體對象
  • 對象的訪問定位

    • 使用句柄:Java堆中會分配一塊內存做爲句柄池,reference存儲的是句柄地址

      image.png

    • 直接指針---HotSpot使用方式

      image.png

      • 比較:

        • 使用句柄的最大好處是reference中存儲的是穩定的句柄地址,在對象移動(GC)時只改變實例數據指針地址,reference自身不須要修改
        • 直接指針訪問的最大好處就是速度快,節省了一次指針定位的時間開銷
        • 若是是對象頻繁GC,那麼句柄方式好,若是對象頻繁訪問則直接指針方式更好

二. 垃圾回收機制

  • 程序計數器,虛擬機棧,本地方法棧3個區域隨着線程的生命週期生亡(由於線程私有),棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操做.而java堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期才知道哪些對象會建立,這部份內存的分配和回收都是動態的,垃圾回收所關注的也就是這部份內存

2.1 如何斷定對象爲垃圾

  1. 引用計數法

    • 棧中的引用指向堆中的實例,當引用置爲null時,實例的計數減小1,當計數爲0時,即被回收

      • 通常不使用此方法,由於當幾個實例內部相互引用,而並無棧引用時,仍然不會進行回收,存在循環引用的問題
      • 垃圾回收日誌參數:-verbose:gc -XX:+PrintGCDetails
![image.png](/img/bVbHxNR)
  1. 可達性分析法

    • 經過一系列的"GC Roots"的做爲起始點,從這些節點出發所走過的路徑成爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連的時候說明對象不可用

      • 可做爲GCRoots的對象

        • 虛擬機棧(棧幀中的本地變量表)中引用的對象
        • 本地方法棧中引用的對象
        • 方法區的類屬性所引用的對象
        • 方法區中常量所引用的對象
![image.png](/img/bVbHxNS)
  1. 引用

    • 強引用:相似於Object obj = new Object();建立的,只要強引用存在就不會回收
    • 軟引用:SoftReference類實現軟引用,在系統要發生內存溢出異常以前,將會把這些對象列進回收範圍中進行二次回收
    • 弱引用:WeakReference類實現弱引用,對象只能生存到下一次垃圾回收以前,在垃圾收集器工做時,不管內存是否足夠都會回收只被弱引用關聯的對象
    • 虛引用:PhantomReference類實現虛引用,沒法經過虛引用獲取一個對象的實例,爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知

2.2 如何回收

  • 回收策略

    1. 標記-清除法:

      • 直接標記清除便可
      • 不足

        • 效率不高
        • 空間會產生大量碎片
    2. 複製算法:

      • 把空間分紅兩塊,每次只對其中一塊進行GC,將還存活的對象複製到另外一塊
      • 解決前一種方法的不足,可是會形成利用率低下,由於大多數新生代都不會熬過第一次GC,因此不必1:1劃分空間,能夠劃分爲一塊較大的Eden空間和兩塊較小的Srvior空間,每次使用Eden空間和其中一塊Survior空間.當回收時,將Eden和Sruvior中還存活的對象一次性複製到另外一塊Survior上,最後清理Eden和Survior空間,大小比例爲8:1:1,每次只浪費10%的Survior空間
      • 可是這裏有一個問題就是若是存活的大於10%時就會有問題,這時就能夠採起一種分配擔保策略:多餘的對象直接進入老年代
    3. 標記-整理法

      • 不一樣於針對新生代的複製算法,針對老年代的特色,建立該算法,主要把存活的對象移到內存的一端
    4. 分帶收集法

      • 根據存活對象劃分幾塊內存區,通常分爲新生代和老年代,而後根據各個年代的特定定製相應的回收算法
      • 新生代每次垃圾回收都有大量的對象死去,只有少許對象存活,因此使用複製算法比較合理
      • 老年代中對象存活率高,沒有額外的空間分配對它進行擔保,因此必須使用標記-整理算法

2.3 什麼時候回收

  1. serial

    • 這是一個單線程收集器,意味着它只會使用一個CPU或一條收集線程去完成收集工做,而且在進行垃圾回收時必須暫停其它全部的工做線程直到收集結束

image.png

  1. parnew

    • 能夠認爲是Serial收集器的多線程版
    • Parallel(並行):指多條垃圾收集線程並行工做,此用戶線程處於等待狀態
    • Concurrent(併發):指用戶線程和垃圾回收線程同時執行(不必定並行,有多是交叉執行),用戶進程在運行,而垃圾回收線程在另外一個CPU上運行

image.png

  1. Parallel Scavenge;

    • 這是一個新生代收集器,也是使用的複製算法實現,同時也是並行的多線程收集器
    • CMS等收集器的關注點是儘量的縮短垃圾收集時用戶線程等待的時間,而Parallel Scavenge收集器的目的是達到了一個可控制的吞吐量(用戶線程運行時間/(用戶線程運行的時間 + 垃圾收集的時間)
    • 做爲一個吞吐量優先的收集器,虛擬機會根據當前系統的運行狀況,動態調節停頓時間

image.png

  1. Cms

    • CMS(Concurrent Mark Sweep)收集器是一種以得到最短停頓時間爲目標的收集器
    • 運行步驟

      1. 初始標記(CMS initial mark):標記GC Roots能直接關聯到的對象
      2. 併發標記(CMS Concurrent mark):進行GC Roots Tracing
      3. 從新標記(CMS Remark):修正併發標記期間的變更部分
      4. 併發清除(CMS concurrent sweep)
    • 缺點:對CPU資源敏感,沒法收集浮動垃圾

image.png

  1. G1

    • 面向服務端的垃圾回收器
    • 運行步驟

      1. 初始標記(initial Marking)
      2. 併發標記(Concurrent Marking)
      3. 最終標記(Final Marking)
      4. 篩選回收(Live Data Concurrent And Evacuation)
    • 優勢:並行與併發,分代收集,空間整合,可預測停頓等

image.png

2.4 內存分配和回收策略

  • Java對象分配流程

    • 對象不在堆上分配的主要緣由是由於堆共享,在堆上須要新增同步開銷,不管是TLAB仍是棧都是線程私有的,私有即避免了鎖的競爭,這是典型的空間換效率的作法

image.png

  1. 編譯器經過逃逸分析,確地對象是在棧上分配仍是在堆上分配,若是是在對上分配,則嘗試TLAB分配
  2. 若是TLAB_top + size <= TLAB_end,則在TLAB上直接分配對象並增長TLAB_top的值,若是TLAB不足以存放當前對象,則從新申請一個TLAB,並嘗試再次存放當前對象
  3. 若是仍然放不下,則在Eden區加鎖(這個區是多線程共享的),若是Eden_top + size <= Eden_end,則將對象存放在Eden區,增長Eden_top的值
  4. 若是Eden區放不下,則執行一次Minor GC,若是Eden區仍然放不下,則直接分配到老年代
  • 對象分配原則

    1. 優先分配到Eden區

      • 對象主要分配在新生代的Eden區,若是啓動了本地線程分配緩衝區TLAB,則優先分配在TLAB上,少數狀況會直接分配到老年代中

        • 新生代GC(Minor GC):發生在新生代的垃圾回收動做,頻繁,速度快
        • 老年代GC(Major/Full GC):發生在老年代的垃圾回收動做,出現Full GC常常會伴隨至少一次Minor GC(非絕對),Major GC的速度通常會比Minor GC慢十倍以上

image.png

/*
-verbose:gc -XX:PrintGCDetails 表示輸出虛擬機中GC的詳細狀況
-Xms20M -Xmx20M -Xmn10M 設置內存大小爲20M,新生代大小爲10M
-XX:SurviorRatio=8 設置Eden和Survior的比值大小爲8:1
*/

public static void main(String[] args){
    byte[] b1 = new byte[2 * 1024 * 1024];
    byte[] b2 = new byte[2 * 1024 * 1024];
    byte[] b3 = new byte[2 * 1024 * 1024];
    byte[] b4 = new byte[4 * 1024 * 1024];//第一次MinorGC
    
    System.gc();
}

JVM優先把對象放入Eden區,當Eden區放不下後(2 * 3 = 6),經過分配擔保機制放入老年代6M(Minor GC),再把最後一個4M放入新生代
  1. 大對象直接分配到老年代
  2. 長期存活的對象分配到老年代
  3. 空間分配擔保
  4. 動態對象的年齡判斷
  • GC觸發時機

    • Minor GC:

      • minorGC:1.Eden區滿了或者新建立的對象大於Eden區的剩餘空間
    • Full GC:

      • 當調用System.gc時,系統建議執行fullGC,但不是必然執行
      • 老年代空間不足
      • 經過minorGC進入老年代的數據大於老年代剩餘的空間

2.5 GC優化

  • GC優化的兩個目標

    1. 將進入老年代的對象數量降到最低-----減小Full GC的頻率

      • 對象在Eden區被建立,隨後被轉移到了Survior區,所以以後剩餘的對象會被轉入老年代,也有一些對象因爲佔用內存過大,在Eden區被建立後直接進入老年代,老年代GC相對來講比新生代GC更加耗時,所以,減小進入老年代的對象數量能夠顯著提升Full GC的頻率
    2. 減小Full GC的時間

      • Full GC的執行時間比Minor GC要長不少,所以,若是在Full GC上花費過多的時間(超過1s),將可能出現超時錯誤.能夠經過減少老年代內存大小使得Full GC的時間下降,可是減少老年代的內存大小又會增長Full GC的頻率,因此二者須要一個平衡
  • 優化方向:

    1. GC優化是最後不得已才採用的手段
    2. 通常來講堆越大越好,可以下降GC的頻率,但增長堆內存,會形成單次GC須要遍歷處理的對象更多,耗時更長,也會受服務器硬件的限制沒法無限大,因此須要找到一個平衡點
    3. 一般堆參數-Xms和-Xmx能夠設置相等,放置垃圾收集器在最小和最大之間收縮堆而產生額外的消耗,消耗性能
    4. 新生代/老年代大小比例合適:

      • 新生代太小,發生Minor GC頻繁,且大對象容易直接進入老年代
      • 新生代過大,老年代變小,容易Full GC頻繁,Minor GC耗時大幅度增長
      • 建議新生代/老年代比例爲3/8

2.6 常見的JVM異常

  • OutOfMemory(OOM)

    • OutOfMemory,即內存溢出,是一個常見的JVM問題,通常知足如下兩個條件時會拋出

      1. JVM 98%的時候會花費在內存回收
      2. 每次回收的內存小於2%
    • 三種OOM異常

      1. OutOfMemoryError:Java heap space - 堆空間溢出
      2. OutOfMemoryError:PermGen space - 方法區和運行時常量溢出
      3. OutOfMemoryError:unable to create new native thread - 線程沒法建立
  1. OutOfMemoryError:Java heap space:表示堆空間溢出

    • 緣由:JVM 分配給堆內存的空間已經滿了
    • 問題定位

      1. 使用jmap或-XX:+HeapDumpOnOutOfMemoryError獲取堆快照
      2. 使用內存分析工具(visualvm,mat,jProfile)對堆快照進行分析
      3. 根據分析圖,重點是確認內存中的對象是否必要的,分清到底是內存泄漏(Memory Leak)仍是內存溢出
    • 內存泄漏

      • 沒用了的內存沒有及時釋放致使最後佔滿內存
    • 內存泄漏的幾個常見狀況

      1. 靜態集合類:聲明爲靜態的HashMap,Vector等集合,一般來說A中有B,當前只把B設置爲空,A沒有設置爲空,回收時B沒法回收---由於被A引用
      2. 監聽器:監聽器被註冊後釋放對象沒有刪除監聽器
      3. 物理鏈接:DataSource.getConnect()創建的鏈接,必須經過close()關閉鏈接
      4. 有死循環或沒必要要的重複建立大量對象
  2. OutOfMemoryError:PermGen space:表示方法區和運行時常量池溢出

    • 緣由

      • perm區主要存放Class和Meta信息,Class在被Loader時就會被放到PermGen space,這個區域稱爲永久代,GC在主程序運行期間不會對永久代進行清理,默認64M
      • 當程序中使用了大量的jar或class,使java虛擬機裝載類的空間不夠,超過64M就會報出這部份內存溢出了,須要加大內存分配
    • 解決方案

      1. 擴大永久代空間

        • JDK7以前使用-XX:PermSize和-XX:MaxMetaspaceSize控制永久代大小
        • JDK8之後把本來放在永久代的字符串常量移到Java堆中(元空間Metaspace)中,元數據並不在虛擬機中,使用的是本地內存

          • 使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize控制元空間
      2. 清理應用程序下WEB-INF/lib下的jar,刪除不用的jar,多個應用公共的jar移動到Tomcat的lib目錄.減小重複加載
  3. 優化參數

    堆配置:
    -Xms:初始堆大小
    -Xms:最大堆大小
    -Xss: 每一個線程的堆棧大小
    -XX:NewSize=n:設置年輕代大小
    -XX:NewRatio=n:設置年輕代和年老代的比值。
    -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。
    -XX:MaxMetaSpaceSize=n:最大元空間大小
    -XX:+CollectGen0First : FullGC時是否先YGC
    收集器設置:
    -XX:+UseSerialGC:設置串行收集器
    -XX:+UseParallelGC:設置並行收集器
    -XX:+UseParalledlOldGC:設置並行年老代收集器
    -XX:+UseConcMarkSweepGC:設置併發收集器
    -XX:ParallelGCThreads 並行收集器的線程數
    打印GC:
    -XX:+PrintGC
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps
    -Xloggc:filename

三. 性能監控工具

  • 虛擬機工具

    • Jps

      • java process status,能夠查看本地虛擬機惟一id lvmid(local virtual machine id)
      • 參數

        • -m:運行時傳入主類的參數
        • -v:虛擬機參數
        • -l:運行時主類全名
    • Jstat

      • 監視虛擬機運行時的狀態信息
      • jstat -gcutil 8080 1000 10:每隔1000毫秒執行1次,一共10次
    • Jinfo

      • 實時查看和調整虛擬機的各項參數
      • 例如:

        • -XX:-UseSerialGC 不啓動Serial垃圾回收器
        • -XX:+UseG1GC 啓用G1垃圾回收器
    • Jmap

      • 生成java程序的dump文件
    • Jhat

      • JVM heap Analysis Tool,十分佔據內存和CPU,使用較少
    • Jstack

      • 生成線程快照,定位線程長時間停頓的緣由
      • 命令:jstack [option] vmid
    • Jconsole

      • 一種基於JMX的可視化監控,管理工具,進行內存管理,線程管理,查看死鎖等

四. 性能調優案例

  1. 案例一:

    • 背景:績效考覈系統,會針對每個考覈員工生成一個各考覈點的考覈結果,造成一個Excel文檔,供用戶下載.文檔中包含用戶提交的考覈點信息以及分析信息,Excel文檔由用戶請求的時候生成,下載並保存在內存服務器一份
    • 問題:常常用戶反映長時間頓卡的問題
    • 處理思路

      • 優化SQL(無效,SQL通常不會出現不規律問題)
      • 監控CPU
      • 監控內存發現常常Full GC 20-30s

        • 運行時產生大對象(每一個教師考覈的數據WorkBook),直接放入老年代,Minor GC不會去清理,會致使Full GC,且堆內存分配太大,時間過長
    • 解決方案:部署多個Web容器,多個Web容器分攤數據,減小內存老年代產生Full GC的次數
  2. 案例二:

    • 背景:簡單數據抓取系統,抓取網絡上的一些數據,分發到其餘應用
    • 問題:不按期內存溢出,把堆內存加大也無濟於事
    • 處理的方法:NIO使用了對外內存,對外內存沒法垃圾回收,致使溢出

五. 類的文件結構

  • 無關性

    • Java語言選擇了與操做系統和機器指令集無關的,平臺中立的格式做爲程序編譯後的存儲格式,Java虛擬機提供的語言無關性是指虛擬機不關心Class的來源是何種語言,只要能生成Class文件就夠了.
    • 可使用Binary Viewer讀取二進制文件

5.1 字節碼格式

  • Java Class文件

    • 8位字節的二進制流,數據項按順序存儲在class文件中,相鄰的項之間沒有間隔,這樣可使class文件緊湊,佔據多個字節的空間的項按照高位在前的順序分爲幾個連續的字節存放,在class文件中
    • 可變長度項的大小和長度位於實際數據以前,這個特性使得class文件流能夠從頭至尾被順序解析,首先讀出項大小,而後讀出項數據
    • class文件中有兩種數據結構,可對比xml或json

      • 無符號數
    • 二進制文件沒有空格和換行,節省空間和提升性能,但放棄了可讀性

image.png

  1. 魔數

    • 每一個Java Class文件的前4個字節被稱爲它的魔數(magic number):0xCAFEBABE
    • 魔數的做用在於,能夠輕鬆的分辨出Java Class文件和非Java Class文件
    • class文件的下面4個字節包含了主,次版本號,對於Java虛擬機來講,版本號肯定了特定的class文件格式,一般只有給定主版本號和一系列次版本號後,Java虛擬機纔可以讀取class文件,如52對應JDK1.8
  2. 常量池

    • constant_pool_count:兩個字節表示常量池的長度,編號從1開始
    • constant_pool:每一個常量池入口都從一個長度爲一個字節的標誌開始(tag),這個標誌指出 了列表中該位置的常量類型,JDK 1.7後共有14種不一樣的表結構數據

image.png

  1. 訪問標誌access_flags

    • 緊接常量池後的兩個字節稱爲access_flags,它展現了文件中定義的類或接口的幾段信息,包括這個Class是類仍是接口
    • 是否認義爲public類型,是否爲abstract類型,在access_flags中全部未使用的位都必須由編譯器置0

image.png

  1. 類索引

    • 接下來兩個字節爲this_class項,它是一個對常量池的索引,在this_class位置的常量池入口必須爲CONSTANT_Class_info表,該表由兩個部分組成-----標籤和name_index

      • 標籤部分是一個具備CONSTANT_Class值的常量
      • name_index位置的常量池入口爲一個包含了類或接口的全限定名的CONSTANT_Utf8_info
    • 父類索引與接口索引集合同理

image.png

  1. 字段表集合

    • 在interfaces後面的是對類或接口中所聲明的字段的描述,首先是field_count的計數,它是類變量和實例變量的字段的數量總和,在這個計數後面是不一樣長度的field_info表的序列(不包括從超類繼承的字段)

      image.png

  2. 方法表集合

    • 緊接fields後面的是對該類或接口中所聲明的方法的描述,只包括在該類或接口中顯式定義的方法

      image.png

  3. 屬性表集合

    • 在Class文件,字段表,方法表中均可以攜帶本身的屬性集合

      • 相對其餘表,屬性表的限制相對較小,再也不要求各個屬性表之間有嚴格的順序,能夠寫入自定義的屬性信息,JVM定義了21項屬性表
      • 每一個屬性,它的名稱都須要從常量池中引入一個Constant_Utf8_info類型的常量表示,而屬性值徹底自定義,只須要一個u4的長度屬性去說明屬性值所佔用的位數便可
![image.png](/img/bVbHxOk)

5.2 字節碼指令

  1. 加載和存儲指令

    • 加載和存儲指令用於將數據在棧幀中的局部變量表和操做數棧之間來回傳輸

      • 將一個局部變量加載到操做數棧

        • iload,lload,fload,dload,aload
      • 將一個數值從操做數棧存儲到局部變量表

        • istore,lstore,fstore,dstore,astore
      • 將一個常量加載到操做數棧

        • bipush:將一個byte類型數據入棧
        • ldc:從運行時常量池中提取數據壓入操做數棧
  2. 運算指令

    • 運算指令用於對兩個操做數棧上的值進行某種運算,並把結果從新存儲到操做數棧頂,運算指令主要分爲:整型數據運算和浮點型數據運算,其中如下x=i,l,f,d分別表示int型,long型,float型,double型

      • 加法指令:xadd
      • 減法指令:xsub
      • 乘法指令:xmul
      • 除法指令:xdiv
      • 求餘指令:xrem
      • 取反指令:xneg
      • 位移指令:xshl,xshr,xushr
      • 按位或指令:xor
      • 按位於指令:xand
      • 按位異或指令:xxor
      • 局部變量自增指令:xinc
      • 比較指令:xcmpl
  3. 類型轉換指令

    • 類型轉換指令能夠將兩種不一樣數值類型進行互相轉換,Java虛擬機直接支持(無需轉換指令)寬化類型轉換(小範圍向大範圍類型的安全轉換)

      • int---long---float---double
    • 處理窄化類型轉換時,須要使用相關指令

      • i2b,i2c,i2s,d2i
  4. 對象建立和訪問指令

    • 在java中類實例和數組都是對象,可是JVM對類Class對象和數組對象的建立使用了不一樣的字節碼指令

      • 建立類實例的指令:new
      • 建立數組的指令:newarray
      • 訪問類變量(static字段)的指令:getstatic,putstatic
      • 訪問實例變量的指令:getfield,putfield
      • 將一個數組元素加載到操做數棧的指令:baload,caload
      • 將一個操做數棧的值存到數組元素中的指令:bastore,castore
      • 取數組長度的指令:arraylength
      • 檢查類實例類型的指令:instanceof,checkcast
  5. 操做數棧管理指令

    • 將操做數棧棧頂n個元素出棧:pop(n)
    • 複製棧頂1個或2個數值,並將複製的值從新壓入棧頂:dup,dup2
    • 將棧頂兩個數值互換:swap
  6. 控制轉移指令

    • 控制轉移指令可讓Java虛擬機從指定位置的指令繼續執行(而不是當前指令的下一條),因此從概念模型上理解,能夠認爲控制轉移指令就是有條件和無條件修改PC寄存器的值

      • 條件分支:ifeq,iflf,ifgt
      • 複合條件分支:tableswitch,lookupswitch
      • 無條件分支:goto
  7. 方法調用指令和返回指令

    • 方法調用指令(分派,執行過程)

      • invokevitual:用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是java語言中最多見的方法分派方式
      • invokeinterface:用於調用接口方法,它會在運行時搜索一個實現了此接口的對象,找出合適的方法進行調用
      • invokespecial:用於調用一些須要特殊處理的實例方法,包括:實例化初始化方法,私有方法和父類方法
      • invokestatic:調用static方法
      • invokedynamic:指令用於在運行時動態解析出調用點限定符引用的方法,並執行該方法,前面的4條調用指令的分派邏輯都固話在java虛擬機內部,而invokedynamic指令的分派邏輯由用戶所設定的引導方法決定
    • 方法返回指令

      • 方法調用指令跟數據類型無關,方法返回指令是根據返回值類型區分的

        • return:提供聲明爲void的方法,實例初始化方法,類和接口的類初始化方法使用
        • ireturn:提供int類型的數據,當返回值是boolean,byte,char,shot和int時使用
        • 其餘類型的返回指令:lreturn,freturn,dreturn,areturn
  8. 異常處理指令

    • 在java程序中顯示拋出異常的操做(如throw語句)都由athrow指令來實現,除了用throw語句顯示拋出的異常外,java虛擬機還規定了許多字JVM檢查到異常時自動拋出的運行時異常
    • java虛擬機中處理異常(如catch語句)不是由字節碼指令實現的,而是採用異常處理器(異常表)完成的
  9. 同步指令

    • java虛擬機能夠支持方法級的同步和方法內部一段指令的同步,二者同步都是使用管程(Monitor)來支持的

      • 方法級的同步

        • 方法級的同步是隱式的,即無需經過字節碼指令控制,它實如今方法調用和返回操做之中,虛擬機能夠從方法常量池的方法表中ACC_SYNCHRONIZED訪問標誌得知此方法是否聲明爲同步方法,
        • 當方法調用時,若是此方法爲同步方法,則執行線程就要去先成功持有管程,而後才能執行方法,方法(不管成功與否)完成後釋放管程,若是這個同步方法執行期間拋出異常,而且方法內部沒法處理,那麼方法持有的管程將在異常拋出去後自動釋放
      • 指令序列級的同步

        • 同步一段指令序列一般由Java中的synchronized語句塊來表示的,Java虛擬機指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字

六. 類加載機制

6.1 類加載的生命週期

  1. 類加載的時機

    image.png

    • 加載-驗證-準備-初始化-卸載這五個階段的順序是肯定的,但解析階段則不必定,它在某些狀況下能夠在初始化階段以後進行,虛擬機規範嚴格規定了有且只有5種狀況必須對類進行"初始化"

      1. 使用new關鍵字實例化對象的時候,讀取或設置一個類的靜態字段的時候,已經調用一個類的靜態方法的時候
      2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有初始化,則須要先觸發其初始化
      3. 當初始化一個類的時候,若是發現其父類沒有初始化就會先初始化它的父類
      4. 當虛擬機啓動的時候,用戶須要指定一個要執行的任務(就是包含main()方法的那個類),虛擬機會先初始化這個類
      5. 使用jdk 1.7動態語言支持的時候的一些狀況
    • 除以上五種以外的引用類的方式都不會觸發初始化,稱爲被動引用

      1. 經過子類引用父類靜態字段,不會致使子類初始化
      2. 經過數組定義引用類,不會觸發此類的初始化
      3. 常量在編譯器階段會存入調用類的常量池,本質上沒有直接引用定義常量的類,所以不會觸發定義常量的類的初始化
  2. 加載

    • 加載過程

      1. 經過類的徹底限定名,產生一個表明該類型的二進制數據流

        1. 文件(Class文件,Jar文件)
        2. 網絡
        3. 計算機生成(代理Proxy)
        4. 由其餘文件生成(jsp)
        5. 數據庫
      2. 解析這個二進制流爲方法區內的運行時數據結構
      3. 建立一個表示該類型的java.lang.Class類的實例,做爲方法區這個類的各類數據的訪問路口
  3. 驗證

    • 驗證是鏈接階段的第一步,這一階段的目的就是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全
    • 從總體上看,驗證階段大體上會完成4個階段的校驗工做

      1. 文件格式

        • 是否以魔數0xCAFEBABE開頭
        • 主,次版本號是否在當前虛擬機的處理範圍內
        • 常量池的常量是否有不被支持的類型
        • 指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量
        • CONSTANT_Utf8_info型的常量是否有不符合UTF8編碼的數據
        • Class文件中各個部分文件自己是否有被刪除的附加信息
      2. 元數據

        • 這個類是否有父類(除了Object外)
        • 這個類的父類是否繼承了不容許被繼承的類(final修飾的類)
        • 若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法
        • 類中的字段,方法是否與父類中的產生矛盾(覆蓋父類final字段,出現不符合規範的蟲重載)
      3. 字節碼

        • 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做(不會出現按long類型讀取一個int數據)
        • 保證跳轉指令不會跳轉到方法體之外的字節碼指令上
        • 保證方法體中的類型轉換是有效的(子類對象複製給父類數據類型是安全的,反之不合法)
      4. 符號引用

        • 符號引用中經過字符串描述的全限定名是否能找到對應的類
        • 符號引用中的類,字段,方法的訪問性
  4. 準備

    • 準備階段正式爲類變量分配內存並設置類變量初始值,這些變量的內存在方法區中分配(含static修飾的變量不含實例變量)

      • 如:publci static int value = 1122; 這句代碼在初始值設置以後爲0,由於這時候並未開始執行任何java方法,而把value複製爲1122的putstatic指令是程序被編譯後,存放於clinit()方法中,因此初始化階段纔會對value賦值
  5. 解析

    • 這個階段是虛擬機將常量池內的符號引用替換爲直接引用的過程

      • 符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,符號引用與虛擬機實現的內部佈局無關,引用的目標並不必定已經加載到了內存中
      • 直接信用:直接引用可使直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄,直接引用和快速的內存佈局有關,引用的目標一定已經加載到了內存中
  6. 初始化

    • 前面過程都以虛擬機爲主導,而初始化階段開始執行類的java代碼

      • 初始化階段是執行構造器<clinit>()方法的過程,它是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中語句合併產生.靜態語句塊中只能訪問定義在靜態語句塊以前的變量
      • 父類中定義的靜態語句塊要因爲子類的變量賦值操做
      • 若是一個類沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類產生<clinit>()方法
      • 虛擬機會保證一個類的<clinit>()方法在多線程的環境中被正確的加鎖,同步,若是多線程同時去初始化一個類,線程去執行這個類的<clinit>()方法,其餘的線程都須要阻塞等待,直到活動線程的<clinit>()方法結束

6.2 類加載和對象建立流程

  1. 啓動JVM,開始分配內存空間
  2. 開始加載Test.class文件,加載到方法區中,在加載的過程當中靜態的內容要進入靜態區中
  3. 在開始運行main方法,這時JVM就會把main調用到棧中運行,開始從方法的第一行往下執行
  4. 在main方法中new Child(); 這時JVM就會在方法區中查找有沒有Child文件,若是沒有就加載Child.class文件,若是Child繼承Parent類,那麼也須要查找有沒有Parent文件,若是沒有也須要加載Parent.class文件
  5. Child.class和Parent.class中全部的非靜態內容會加載到非靜態的區域中,而靜態的內容會加載到靜態區中,靜態內容(靜態變量,靜態代碼塊,靜態方法)

    • 類的加載只會執行一次,下次再建立對象時,能夠直接在方法區中獲取class信息
  6. 開始給靜態區中的全部靜態的成員變量開始分配內存和默認初始值
  7. 以後給全部的靜態成員變量顯示初始化和執行靜態代碼塊---<clinit>()方法

    • 靜態代碼塊時在類加載的時候執行的,類的加載只會執行一次因此靜態代碼塊也只會執行一次
    • 非靜態代碼塊和構造函數中的代碼是在對象建立的時候執行的,所以對象建立(new)一次,它們就會執行一次
  8. 這時Parent.class文件和Child.class文件加載完成
  9. 開始在堆中建立Child對象,給Child對象分配內存空間,其實就是分配內存地址
  10. 開始對類中的非靜態的成員變量開始默認初始化
  11. 開始加載對應的構造方法,執行隱式三步

    1. 隱式的super()
    2. 顯示初始化(給全部的非靜態成員變量)
    3. 執行構造代碼塊
    4. 執行本類的構造方法
  12. 對象建立完成,把內存的地址賦值給引用對象使用
  13. 若是後續又建立(new)一個新的Child對象,重複步驟9以後的步驟

七. 字節碼執行引擎

  1. 運行時的棧幀結構

    • 棧幀存儲了方法的局部變量表,操做數棧,動態連接和方法返回地址等信息,每個方法從調用開始到執行結束的過程,都對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程

      image.png

  2. 局部變量表

    • 局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量,局部變量表的容量以變量槽(Variable Slot)爲最小單位,一個Slot能夠存放一個32位之內的數據類型
    • reference類型表示一個對象實例的引用,對於 64位的數據類型(long和double),虛擬機會以高位對齊的方式爲其分配兩個連續的Slot空間,是線程安全的
  3. 操做數棧

    • 是一個後入先出棧,當一個方法執行開始時,這個方法的操做數棧是空的,在方法執行的過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧/入棧操做
  4. 動態連接

    • 每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這引用是爲了支持方法調用過程當中的動態連接
  5. 方法返回地址

    • 方法退出的過程實際上等同把當前棧出棧,所以退出時可能執行的操做有:

      • 恢復上層方法的局部變量表和操做數棧
      • 把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調用PC計數器的值以指向方法調用指令後面的一條指令
  6. 方法調用---解析

    • 方法調用並不等同於方法的執行,方法調用階段的惟一任務就是肯定被調用方法的版本(繼承和多態)
    • "編譯期可知,運行期不可變"的方法(靜態方法和私有方法),在類加載的解析階段,會將其符號引用轉化爲直接引用(入口地址),這類方法的調用稱爲解析
  7. 方法調用---分派

    • 靜態分派最典型的應用就是方法重載
    • 在運行期根據實際類型肯定方法執行版本的分派過程稱爲動態分派,最典型的應用就是方法重寫

八. 類加載器

8.1 JVM類加載器

image.png

  1. Java虛擬機自帶了如下幾種加載器

    1. 根(Bootstrap)類加載器

      • 該加載器沒有父加載器,它負責加載虛擬機的核心類庫,如:java.lang.*等,根類加載器從系統屬性sun.boot.class.path所指定的目錄中加載類庫,根類加載器的實現依賴於底層操做系統,屬於虛擬機的實現的一部分,它並無繼承java.lang.ClassLoader類
    2. 擴展(Extension)類加載器

      • 它的父加載器爲根類加載器,它從java.ext.dirs系統屬性所指定的目錄加載類庫,或從JDK安裝目錄jre/lib/ext子目錄下加載類庫,若是把用戶建立的JAR文件放在這個目錄下,也會自動由擴展類加載器加載,擴展類加載器是純Java類,是java.lang.ClassLoader類的子類
    3. 系統(System)類加載器

      • 也稱爲應用類加載器,它的父加載器爲擴展類加載器,它從環境變量classpath或系統屬性java.class.path所指定的目錄中加載類,它是用戶自定義的類加載器的默認父加載器,系統類的加載器是純Java類,是java.lang.ClassLoader類的子類
  2. 父委託機制案例:

    1. 當自定義一個java.lang.String類的時候,當主動使用自定義String類,實例化的是java定義的String類
    2. 這是由於當類加載器區初始化類時,會一層一層往上委託,先由Bootstrap類加載器區初始化,若找不到再由Extendsion類加載器去初始化,最後都找不到字節碼文件時,再由應用加載器去初始化
    3. 若class存在classpath系統路徑中,那麼就會由系統類加載器去初始化,不能由咱們本身定義的加載器初始化,只有在classpath下不存在才能使用自定義類加載器

      public static void main(String[] args){
          Class<?> clazz = Class.forName("java.lang.String");
          System.out.print(clazz.getClassLoader); //null
      }
      
      public class String{
          
          static {
              System.out.println("my custom String class");
          } 
      }
      • 因此在加載String類時使用的是Bootstrap類加載器,若自定義的類加載器優先級更高,那麼繼承這個類的全部類都會受到影響,因此父委託機制就避免了這種安全性問題
      • 父委託機制的優勢:能提升提系統的安全性,在此機制下,用戶自定義的類加載器不可能加載應該由父加載器加加載的可靠類,所以能夠防止惡意代碼替代父加載器的可靠代碼
      • 父子類加載器之間的真實關係---包裝關係

8.2 自定義類加載器

  • 自定義類加載器

    • 使用defineClass()方法
    • 重寫findClass()方法
    • 對外調用loadClass()方法
    public class MyClassLoader extends ClassLoader{
        private final static String DEFAULT_DIR = "D:\\classloader";
        private String dir = DEFAULT_DIR;
        private String classLoaderName;
        
        public MyClassLoader(){
            super();
        }
        
        public MyClassLoader(String classLoaderName){
            super();
            this.classLoaderName = classLoaderName;
        }
        
        public MyClassLoadeR(String classLoaderName,ClassLoader classLoader){
            super(classLoader);
            this.classLoaderName = classLoaderName;
        }
        
        @Override
        protected Class<?> findClass(String name) throw ClassNotFoundException{
            String classPath = name.replace(".","/");
            File classFile = new File(dir,classPath + ".class");
            if(!classFile.exists()){
                throw new ClassNotFoundException("the class" + name + "not found");
            }
            
            byte[] classBytes = loadClassBytes(classFile);
            if(null == classBytes || classBytes.length == 0){
                throw new ClassNotFoundException("the class" + name + "load failed");
            }
            
            return this.defineClass(name,classBytes,0,classBytes.length);
        }
        
        private byte[] loadClassBytes(File file){
            try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
                FileInputStream fis = new FileInputStream(file)){
                byte[] buffer = new byte[1024];
                int len;
                while((len = fis.read(buffer)) != -1){
                    bos.write(buffer,0,len);
                }
                bos.flush();
                return bos.toByteArray();
            }catch(IOException e){
                e.printStackTrace();
                return null;
            }
        }
    }
    
    --------------------------
        publi static void main(String[] args){
        MyClassLoader loader = new MyClassLoader("loader1");
        load1.setDir("D:\\classloader");
        Class<?> aclass = load1.loadClass("com.lsy.Demo1");
        System.out.println(aclass);
        System.out.println((MyClassLoader)aclass.getClassLoader().getClassLoaderName());
    }
    1. loadClass():是加載 類名.class字節碼文件的工具
    2. findClass():是類加載器在JVM內部實現查找指定路徑下.class文件的機制

      1. Bootstrap---Ext---App,按照這個順序進行查找
      2. 而自定義類加載器就是複寫了該方法,將指定目錄下的字節碼文件,經過ByteArrayOutputStream解密後的字節碼文件給JVM去加載
    3. defineClass():是將你定義的字節碼文件通過字節數組流解密後,將該字節數組流生成字節碼對象,也就是該類的 類名.class
    • loadClass():判斷是否已加載,使用雙親委派機制,請求父加載器,使用findClass()

      • finaClass():過呢局名稱和位置加載.class字節碼的,使用defineClass()方法

        • defineClass():解析定義.class字節流,返回class對象

8.3 打破雙親委託機制

  • 自定義一個類加載器,在類加載器中同時重寫loadClass()方法和findClass()方法,外界調用loadClass()方法不是從父類繼承來的,而實子類本身的
@Override
protexted Class<?> loadClass(String name,boolean resolve) throw ClassNotFoundException{
    Class<?> clazz = null;
    
    if(name.startWith("java.")){
        try{
            ClassLoader system = ClassLoader.getSystemClassLoader();
            clazz = System.loadClass(name);
            if(clazz != null){
                if(resolve){
                    resolveClass(clazz);
                }
                return clazz;
            }
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }
    }
    
    try{
        clazz = findClass(name);
    }catch(Exception e){
        
    }
    if(clazz == null && getParent() != null){
        getParent().loadClass(name);
    }
    
    return clazz;
}
  • 注:

    1. 可是對於java.lang包下的類仍然是不能自定義的,由於此包下的類是不容許重名的,因此想自定義java.lang.String來測試雙親委派機制是不行的

8.4 名稱空間和運行時包

  1. 命名空間

    • 類加載器的命名空間

      1. 每一個類的加載器都有本身的命名空間,命名空間由該加載器及其全部父加載器所加載的類組成
      2. 在同一個命名空間中,不會出現完成的名字
      //Boot.Ext.App.SimpleClassLoader.com.lsy.Demo
  2. 運行時包

    • 運行時包 = 命名空間 + 包名 + 類名

      1. 父類加載器看不到子類加載器加載的類
      2. 子加載器加載的類能夠看到父加載器加載的類
      3. 不一樣命名空間下的類加載器之間的類互相不可訪問
  3. 類的寫案子及ClassLoader的卸載

    • JVM中的Class只有知足如下三個條件,才能被GC回收,也就是該Class被卸載(unload)

      1. 該類全部的實例都已經被GC
      2. 加載該類的ClassLoader實例已經被GC
      3. 該類的java.lang.Class對象沒有在任何地方被引用
    • GC的時機咱們是不可控的,一樣對於Class的卸載也是不可控的
    • 實例對象 ----> ClassLoader -----> Class對象

      • 在Class中有一個ClassLoader的引用,因此須要先回收ClassLoader
  4. 注:

    1. 當一個已經被加載的類是沒法被更新的,若是試圖用用一個ClassLoader再次加載同一個類,就會獲得duplicate classdefinition Exception,咱們之惡可以從新建立一個新的ClassLoader實例來再次加載新類,至於原來已經加載的類就不須要管它了,由於它可能還有其餘案例正在使用,只要相關的實例都被回收,那麼JVM就會在適當的時機把類加載器卸載
    2. 如何實現一個工程中不一樣模塊加載不一樣版本的同名JAR包?

      1. 在JVM裏由類名和類加載器區別不一樣的Java類型,所以,JVM容許咱們使用不一樣的加載器加載相同namespace的java類,而實際上這些相同namespace的java類能夠是徹底不一樣的類
      2. 一般咱們都使用默認的類加載器,因此同步類或者同名jar包是惟一的,沒法加載同名jar包的不一樣版本,而在JVM裏不一樣的類加載器能夠加載相同namespace的java類

8.5 自定義加密解密加載器

public final class EncryptUtils{
    private static final byte ENCRYPT_FACTOR = (byte) 0xff;
    
    private EncryptUtils(){
        //empty...
    }
    
    public static void doEncrypt(String source,String target){
        try(FileInputStream fis = new FileInputStream(source);
            FileOutputStream fos = new FileOutputStream(target)){
            int data;
            while((data = fis.read()) != -1){
                fos.write(data ^ ENCRYPT_FACTOR);
            }
            fos.flush();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
----------------------------------------
 
    private byte[] loadClassBytes(File file){
        try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
            FileInputStream fis = new FileInputStream(file)){
            int data;
            while((data = fis.read()) != -1){
                bos.write(data ^ EncryptUtils.ENCRYPT_FACTOR);
            }
            bos.flush();
            return bos.toByteArray();
        }catch(IOException e){
            e.printStackTrace();
            return null;
        }
    }
相關文章
相關標籤/搜索