JVM筆記

JVM

Java是跨平臺的編程語言,一次編譯,到處運行。Java代碼編譯爲字節碼也就是class文件,然後再不同的操作系統上依靠不同的Java虛擬機轉換成不同平臺的機器碼,最終的得到執行。

       第一個程序輸出打印hello world需要經歷如下步奏:

       首先將,java代碼編譯成字節碼,然後通過java helloworld執行,此時java會根據系統找到jvm.cfg,再通過jvm.cfg中相關配置找到jvm.dll,jvm.dll就是jvm的主要實現,找到jvm.dll緊接着就會初始化jvm並且獲取jni接口,jni接口是java本地接口,他能夠找到class文件並且剪裁放jvm,之後找到main方法,最後執行。這就是java文件的編譯執行流程。

       那麼jvm的結構誰怎樣的呢?如下:

       Class文件通過類加載子系統加載之後,將類中的屬性方法數據分別分配到jvm的不同部分。

       Jvm的內存空間包含:

方法區:各個線程的共享區域,存放類信息、常量、靜態常量。

Java堆:也是線程的共享區域,這裏存放有類的實例,並且這裏是jvm中站內存最大的區域。

Java棧:是每個線程私有的區域,線程執行完畢,屬於該線程的java棧會被銷燬。每執行一個方法,會向java棧中壓入一個元素,這個元素叫做棧幀,棧幀中包含了方法的局部變量,用於存放中間狀態的操作棧等等,當遞歸函數的層級太多會引起棧溢出。

本地方法棧:類似於java棧,只不過他是用來表示執行本地方法的,這裏存放了調用本地方法接口的方法。主要用於實現與操作系統、硬件的交互。

PC寄存器:這裏類似於pc(程序計數器),於控制代碼執行的順序。

垃圾收集器將在後面進行詳細介紹。

接下來講jvm的內存模型:

這裏我們稱java堆爲主內存,那麼在主內存區域,各線程共享這塊區域。但是每個線程執行的時候都會有自己的工作內存,當線程自己對變量進行改變賦值等操作的時候不會馬上更新到主內存(堆)中裏面。

如果需要線程間通信,首先要先把線程1中的變量值寫在內存中,然後線程2在讀取,但是實際的代碼運行的時候卻可能在還沒寫入就讀了主內存中的數據,這樣就會出問題,也就是不能保證修改的可見性。

這裏介紹一下java中volatile,這是修飾定義變量的關鍵詞,當volatile修飾變量之後,變量每次更新都會直接寫在內存上,而不只寫在工作區,這樣,線程間數據的可見性就能夠保證了。當然,volatile關鍵字修飾變量a,a=a+1,這樣的操作不是原子操作,所以無法保證他的原子性。但是volatile能夠保證一定程度上的有序性

//x、y爲非volatile變量

//flag爲volatile變量

 

x = 2;        //語句1

y = 0;        //語句2

flag = true;  //語句3

x = 4;         //語句4

y = -1;       //語句5

如上,語句1、2與4、5順序不能保證,但是語句3對1、2不可見,對4、5可見,也就是4、5一定在3之後,保證一定的有序性。

       計算機執行代碼的時候會對代碼進行重排序,如上有序性所說的一樣,代碼執行順序只要不影響結果就可以改變順序。但是改變順序需要遵守java的happen before規則:

1、程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操作happen—before(時間上)後執行的操作。(單線程程序)

    2、管理鎖定規則:一個unlock操作happen—before後面(時間上的先後順序,下同)對同一個鎖的lock操作。(獲取鎖必須要等別人釋放鎖

    3、volatile變量規則:對一個volatile變量的寫操作happen—before後面對該變量的讀操作。(volatile變量的寫之後的讀不會被排序到寫之前)

    4、線程啓動規則:Thread對象的start()方法happen—before此線程的每一個動作。(start()方法一定在線程內容之前)

    5、線程終止規則:線程的所有操作都happen—before對此線程的終止檢測,可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。(線程的終止檢測一定在線程的動作之後)

    6、線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。(interrupt方法必須在中斷之前)

    7、對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。(對象構造完成定在對象的虛構函數之前)

8、傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那麼可以得出A happen—before操作C。(傳遞)

JVM配置參數分爲三類參數:

1、跟蹤參數:用於監控jvm內存使用情況

2、堆分配參數:用於設置堆的大小

3、棧分配參數:用於設置棧區的大小

這三類參數分別用於跟蹤監控JVM狀態,分配堆內存以及分配棧內存。

還有永久區分配參數與棧大小分配參數。

接下來講解垃圾回收算法:

1,應用計數法,當沒有引用指向的對象就會被垃圾回收器清理掉。

2,標記清除法:它是很多垃圾回收算法的基礎,簡單來說有兩個步驟:標記、清除。

標記:遍歷所有的GC Roots,並將從GC Roots可達的對象設置爲存活對象;

清除:遍歷堆中的所有對象,將沒有被標記可達的對象清除;

3,標記壓縮法:就是標記清楚之後對內存進行壓縮空間,使得內存不那麼碎片化。

4,複製算法:將內存分成兩個區域,所有對象放在一個區域,對內存進行標記,存活的標記,之後將標記的複製到另一個區域,將原來的區域進行全部清理。

 

Jvm的垃圾回收器,主要是對堆內存區域進行垃圾回收。

堆內存結構:

       Java堆內存結構包括兩個區域:新生代與老年代。其中新生代包括一個伊甸區與兩個倖存區,兩個倖存區大小完全對稱,如上s0與s1,也可以稱爲from與to兩個區域

垃圾回收器包括:

1,串行收集器:串行收集器就是使用單線程進行垃圾回收。對新生代的回收使用複製算法,對老年代使用標記壓縮算法。

2,並行回收器:並行回收器分爲兩種:

1、 ParNew回收器

這個回收器只針對新生代進行併發回收,老年代依然使用串行回收。回收算法依然和串行回收一樣,新生代使用複製算法,老年代使用標記壓縮算法。在多核條件下,它的性能顯然優於串行回收器,

2、 Parallel回收器

依然是並行回收器,但這種回收器有兩種配置,一種類似於ParNEW:新生代使用並行回收、老年代使用串行回收。它與ParNew的不同在於它在設計目標上更重視吞吐量,可以認爲在相同的條件下它比ParNew更優。Parallel回收器另外一種配置則不同於ParNew,對於新生代和老年代均適應並行回收。在進行回收時,應用程序暫停,GC使用多線程併發回收,回收完成後應用程序線程繼續運行。

3, CMS回收器:ConcurrentMark Sweep,併發標記清除。注意這裏注意兩個詞:併發、標記清除。

如下流程:

從上圖可以看到標記過程分三步:初始標記、併發標記、重新標記,併發標記是最主要的標記過程,而這個過程是併發執行的,可以與應用程序線程同時進行,初始標記和重新標記雖然不能和應用程序併發執行,但這兩個過程標記速度快,時間短,所以對應用程序不會產生太大的影響。最後併發清除的過程,也是和應用程序同時進行的,避免了應用程序的停頓。但是這個回收由於在運行的過程中進行,就可能造成回收不測底,於是就會很頻繁的進行垃圾回收。

4, GI回收器。

GI回收器,將堆劃分成獨立的區塊,然後選擇垃圾最多的區塊進行清理。


 

G1相對CMS回收器來說優點在於:

1、因爲劃分了很多區塊,回收時減小了內存碎片的產生;

2、G1適用於新生代和老年代,而CMS只適用於老年代。

類加載器原理:java代碼,會經過編譯器編譯成字節碼文件(class文件),再把字節碼文件裝載到JVM中,映射到各個內存區域中,我們的程序就可以在內存中運行了。

如下是java類裝載流程:

加載:讀取class文件的2進制流,並解析,將元數據等放進方法區,在java堆中生成對應類的對象。

連接過程:分爲3步:

    驗證:檢測class文件的合法性,是否可以裝載

    準備:這個過程會給類分配內存,給類的一些字段設置初始值等,final常量設定確定值。

    解析:將符號引用替換爲直接引用,就是引用地址放到直接引用中。

初始化過程:這裏纔開始真正執行代碼,主要是類的構造,靜態代碼塊的執行,代碼值得設定等。

    主動引用:當如下情況的時候,會立即初始化該類:

new對象時

讀取或設置類的靜態字段(除了被final,已在編譯期把結果放入常量池的 靜態字段)或調用類的靜態方法時;

用java.lang.reflect包的方法對類進行反射調用沒初始化過的類時

初始化一個類時發現其父類沒初始化,則要先初始化其父類

含main方法的那個類,jvm啓動時,需要指定一個執行主類,jvm先初始化這個類

子類繼承父類時的初始化順序

1.首先初始化父類的static變量和塊,按出現順序

2.初始化子類的static變量和塊,按出現順序

3.初始化父類的普通變量,調用父類的構造函數

4.初始化子類的普通變量,調用子類的構造函數

類加載器:是一個抽象類,ClassLoader的實例負責吧java字節碼讀取到jvm當中,可以選擇加載的class流方式文件或者網絡,ClassLoder負責整個加載流程。

    ClassLoder類的就作用就是根據一個指定的類的全限定名,找到對應的Class字節碼文件,然後加載它轉化成一個java.lang.Class類的一個實例.

    大部分java程序會使用以下3種類加載器:

啓動類加載器(BootstrapClassLoader):

這個類加載器負責將<JAVA_HOME>\lib目錄下的類庫加載到虛擬機內存中,用來加載java的核心庫,此類加載器並不繼承於java.lang.ClassLoader,不能被java程序直接調用,代碼是使用C++編寫的.是虛擬機自身的一部分.

擴展類加載器(ExtendsionClassLoader):

這個類加載器負責加載<JAVA_HOME>\lib\ext目錄下的類庫,用來加載java的擴展庫,開發者可以直接使用這個類加載器.

應用程序類加載器(Application ClassLoader):

這個類加載器負責加載用戶類路徑(CLASSPATH)下的類庫,一般我們編寫的java類都是由這個類加載器加載,這個類加載器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也稱爲系統類加載器.一般情況下這就是系統默認的類加載器.

**getParent(),返回時null的話,就默認使用啓動類加載器作爲父加載器

雙親委派模型:

自下向上檢查類是否被加載,一般情況下,首先從AppClassLoader中調用findLoadedClass方法查看是否已經加載,如果沒有加載,則會交給父類,Extension ClassLoader去查看是否加載,還沒加載,則再調用其父類,BootstrapClassLoader查看是否已經加載,如果仍然沒有,自頂向下嘗試加載類,那麼從 Bootstrap ClassLoader到 App ClassLoader依次嘗試加載。

    **同一個類被不同類加載器加載得到的結果是不一樣,這個不同反應在對象的 equals()、isAssignableFrom()、isInstance()等方法的返回結果。

    雙親委託模式當實現類在其他加載器中,而接口在頂層類加載器中,這樣無法找到實際的加載類,那麼就需要Thread.setContextClassLoader(),傳入底層ClassLoader實例。