第二周 JVM運行機制

筆記

  1. JVM啟動流程
    啟動過程如下圖所示:


    bootup.png

    注釋:

    • jvm.cfg的用途:Controls the JVMs which may be picked with startup flags when invoking java or javac。
      當(dāng)前系統(tǒng)的默認(rèn)配置是:
    ```bash
        # List of JVMs that can be used as an option to java, javac, etc.
        # Order is important -- first in this list is the default JVM.
        # NOTE that this both this file and its format are UNSUPPORTED and
        # WILL GO AWAY in a future release.
        #
        # You may also select a JVM in an arbitrary location with the
        # "-XXaltjvm=<jvm_dir>" option, but that too is unsupported
        # and may not be available in a future release.
        #
        -server KNOWN
        -client IGNORE
    ```
    
    • JVM.dll
      在linux上是libjvm.so,路徑:/usr/lib/jvm/java-8-oracle/jre/lib/amd64/server/libjvm.so。并且沒有client目錄,表面在linux都是server模式。
  2. JVM基本結(jié)構(gòu)
    總的結(jié)構(gòu)示意圖如下所示:

    structure.png

    說明:

    • PC寄存器
      每個線程擁有一個,指向下一條指令的地址,執(zhí)行native方法的時值為undefined
      這表明線程是執(zhí)行指令和CPU調(diào)度的基本單位。再復(fù)雜的系統(tǒng),也是從main線程擴展起來的。
    • 方法區(qū)
      JDK7放在Perm區(qū),JDK8放在MetaSpace區(qū)。
      主要用于存放加載的類信息,包括:
      1. 類型的常量值(JDK8已經(jīng)移到Heap上)。
      2. 類的屬性(字段)和方法信息。
      3. 方法字節(jié)碼。
    • Java堆
      1. 應(yīng)用新建的對像、數(shù)組以及常量等都存放在Java堆上。
      2. Java堆可以被所有線程訪問,是最重要的內(nèi)存共享區(qū)域。
      3. 在分代GC算法下,Java堆是分代的。典型的分代如下圖:
        heap_gen.png
    • Java棧
      1. 線程私有。
      2. 存放方法調(diào)用相關(guān)的數(shù)據(jù),包括參數(shù)、局部變量以及返回值。對象、數(shù)組這些分配在堆上,棧里只保留引用。
      3. 棧上也可以分配對象。
        前提條件:對象很?。◣资産yte);開啟逃逸分析(-XX:+DoEscapeAnalysis)
        優(yōu)點:對象在不逃逸的情況下,直接分配在棧上,方法調(diào)用結(jié)束對象即回收,不增加堆的負(fù)擔(dān)。
        缺點:要求對象很小,適用場景有限,用處不大,所以這項技術(shù)也沒有流行起來。
        驗證:跑了資料中的代碼,在啟用逃逸分析后,只發(fā)生了很少幾次GC。如果禁用逃逸分析,會發(fā)生大量的GC。
         public class OnStackTest {
      
             public static void alloc(int n) {
                 byte[] b = new byte[n];
                 b[0] = 1;
             }
      
             public static void main(String[] args) throws InterruptedException {
                 for (int i = 0; i < 500000000; i++) {
                     alloc(2);
                 }
             }
         }
      
      開啟逃逸分析:
         dalton@fish ~$ java -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC OnStackTest 
         [GC (Allocation Failure)  2048K->376K(9728K), 0.0012055 secs]
         [GC (Allocation Failure)  2424K->360K(9728K), 0.0018963 secs]
      
      禁用逃逸分析:
         dalton@fish ~$ java -Xmx5m -Xms5m -XX:-DoEscapeAnalysis -XX:+PrintGC OnStackTest 
         [GC (Allocation Failure)  1024K->408K(5632K), 0.0091321 secs]
         [GC (Allocation Failure)  1432K->352K(5632K), 0.0016615 secs]
         [GC (Allocation Failure)  1376K->352K(5632K), 0.0011853 secs]
         [GC (Allocation Failure)  1376K->352K(5632K), 0.0017079 secs]
         [GC (Allocation Failure)  1376K->368K(5632K), 0.0017921 secs]
         [GC (Allocation Failure)  1392K->352K(5632K), 0.0010113 secs]
         [GC (Allocation Failure)  1376K->292K(5632K), 0.0010523 secs]
         [GC (Allocation Failure)  1316K->292K(5632K), 0.0020685 secs]
         [GC (Allocation Failure)  1316K->292K(5632K), 0.0019285 secs]
         [GC (Allocation Failure)  1316K->292K(5632K), 0.0020571 secs]
         [GC (Allocation Failure)  1316K->292K(5632K), 0.0030951 secs]
         [GC (Allocation Failure)  1316K->292K(5632K), 0.0014476 secs]
         [GC (Allocation Failure)  1316K->292K(5632K), 0.0007672 secs]
      
      默認(rèn)會開啟逃逸分析的哦
         dalton@fish ~/a_dev/d_java/perf $ java -Xmx10m -Xms10m -XX:+PrintGC OnStackTest 
         [GC (Allocation Failure)  2048K->392K(9728K), 0.0012030 secs]
         [GC (Allocation Failure)  2440K->384K(9728K), 0.0018770 secs]
      
  3. 內(nèi)存模型
    內(nèi)存模型,即我們常說的JMM,描述了內(nèi)存共享變量的讀寫及可見性。要點:

    • 每個線程都有自己的工作內(nèi)存。
    • 共享變量存放在主內(nèi)存中,與各線程的工作內(nèi)存獨立。
    • 線程要讀取或?qū)懭牍蚕碜兞浚髯远家?jīng)過另個步驟。
      讀:read,load。read是從共享內(nèi)存中讀到工作內(nèi)存;load是從工作內(nèi)存中加載到變量。
      寫:store,write。store是從變量保存到工作內(nèi)存;write是從工作內(nèi)存寫到共享內(nèi)存。
      這個過程的示意圖如下:


      jmm.png
    • 要做到線程間可見(或同步),可以用以下幾種方法:
      1. 用volatile關(guān)鍵字修飾變量。
      2. 用synchronized關(guān)鍵字修飾方法或代碼塊。線程在進入synchronized代碼塊時會從共享內(nèi)存中讀取變量值,離開時會寫入變量值。這就保證了執(zhí)行完synchronized代碼塊后對共享變量的修改是可見的。
      3. 使用常量。
  4. 指令重排
    編譯器和執(zhí)行器都會基于一定的優(yōu)化原則對指令進行重排。其結(jié)果是指令的實際執(zhí)行順序不一定是我們代碼中看到的順序。重排是一種重要的優(yōu)化手段,但同時也增加了線程間同步的困難。因重排的條件比較復(fù)雜,我們倒是可以記住不發(fā)生重排的幾種情況:

    • 寫后讀
    • 讀后寫
    • 寫后寫

    編譯器不考慮多線程間的語義。即它不會考慮多線程間的同步。

    指令重排的基本原則:

    • 程序順序原則:一個線程內(nèi)保證語義的串行性
    • volatile規(guī)則:volatile變量的寫,先發(fā)生于讀
    • 鎖規(guī)則:解鎖(unlock)必然發(fā)生在隨后的加鎖(lock)前
    • 傳遞性:A先于B,B先于C 那么A必然先于C
    • 線程的start方法先于它的每一個動作
    • 線程的所有操作先于線程的終結(jié)(Thread.join())
    • 線程的中斷(interrupt())先于被中斷線程的代碼
    • 對象的構(gòu)造函數(shù)執(zhí)行結(jié)束先于finalize()方法
  5. 編譯與解釋運行的概念
    解釋運行:讀一句執(zhí)行一句字節(jié)碼。
    編譯運行:將字節(jié)碼編譯成機器碼,然后執(zhí)行。

理解與思考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 垃圾回收算法具體實現(xiàn) 翻譯原文 => plumbr Java GC handbook 前文參見: Java垃圾回收...
    foxracle閱讀 2,997評論 0 15
  • 作者:一字馬胡 轉(zhuǎn)載標(biāo)志 【2017-11-12】 更新日志 日期更新內(nèi)容備注 2017-11-12新建文章初版 ...
    beneke閱讀 2,331評論 0 7
  • 內(nèi)存溢出和內(nèi)存泄漏的區(qū)別 內(nèi)存溢出:out of memory,是指程序在申請內(nèi)存時,沒有足夠的內(nèi)存空間供其使用,...
    Aimerwhy閱讀 808評論 0 1
  • JVM架構(gòu)圖分析 JVM被分為三個主要的子系統(tǒng) (1)類加載器子系統(tǒng)(2)運行時數(shù)據(jù)區(qū)(3)執(zhí)行引擎 1. 類加載...
    匆匆歲月閱讀 1,096評論 1 19
  • 我們常期待一份純粹的吸引,年少的不顧一切,迫不及待想與你為伍。那時我,喜歡秀發(fā)拂過手腕時你的姿態(tài),喜歡窗邊叼著煙的...
    木木木t閱讀 332評論 0 0

友情鏈接更多精彩內(nèi)容