Java隨筆(6):JVM的梳理記錄

轉載請注意:http://blog.csdn.net/wjzj000/article/details/76641519

本菜開源的一個自己寫的Demo,希望能給Androider們有所幫助,水平有限,見諒見諒…
https://github.com/zhiaixinyang/PersonalCollect (拆解GitHub上的優秀框架於一體,全部拆離不含任何額外的庫導入)
https://github.com/zhiaixinyang/MyFirstApp(Retrofit+RxJava+MVP)
以及一個可以依賴的自定義驗證碼View:
https://github.com/zhiaixinyang/VerifyCodeView


寫在前面

這是踏入八月的第一篇博客,也是實習後的第一篇博客。多多少少有一些情感摻雜於其中。
這是一篇關於JVM的博客,自己對於JVM的認識,也一直停留在知識點的層面上,這一次綜合的學習記錄一番,也算是對自己基礎知識上的縫補。


開始

編譯

首先,一個java類想要被運行,它就要接受編譯的過程。最直接的方式命令行javac,回車敲下的一瞬間,我們寫下的.java文件便被編譯成.class文件,也就是我們的字節碼文件。

加載

當我們緊接着執行運行.class文件的命令後,系統就會啓動一個JVM進程,JVM進程從classpath路徑中找到.class的二進制文件,將這個類的類信息加載(加載的過程使用到了ClassLoader)到運行時數據區的方法區內,這個過程叫做類的加載。

將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法區中的運行時數據結構,在中生成一個代表這個類的java.lang.Class對象,作爲方法區類數據的訪問入口,這個過程需要類加載器參與。

在段話中加粗了運行時數據區的方法區,這裏邊設計到了JVM的運行時數據區的問題。也就是我們常提到了:堆,方法區,虛擬機棧,程序計數器等(這裏先按住不表,後面我們在娓娓道來)。


緊接着JVM尋找我們的main方法(會在虛擬機棧中的爲這個main方法創建棧幀…),進入後開始執行main方法中的語句,如果這裏我們使用了new,就是讓JVM創建一個對應的對象,如果這時候方法區中沒有這個類的信息,那麼JVM馬上加載這個類,把此類的類型信息放到方法區中。

加載完這個類之後,JVM立即在區中爲一個新的實例分配內存, 然後調用構造方法初始化這個實例,這個實例持有着指向方法區的類的類型信息的引用。

此時如果我們通過這個實例的引用調用了它的某個方法,那麼JVM根據這個實例的引用找到這個對象,然後根據這個對象持有的引用定位到方法區中這個類的類型信息的方法表,獲得這個方法的字節碼的地址。

然後開始執行這個方法。

完畢

一個普通類的編譯加載運行的流程基本就是如此,當然這裏的細節並沒有展開,因此接下來的篇幅就交由這其中的細節來填充。

簡單梳理

(此部分摘自網絡:http://www.cnblogs.com/dooor/p/5289994.html

類加載的全過程,經歷了:加載鏈接驗證準備解析)、初始化使用卸載

  • 加載:將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法區中的運行時數據結構,在堆中生成一個代表這個類的java.lang.Class對象,作爲方法區類數據的訪問入口,這個過程需要類加載器參與。

  • 鏈接:將java類的二進制代碼合併到JVM的運行狀態之中的過程

    • 驗證:確保加載的類信息符合JVM規範,沒有安全方面的問題
    • 準備:正式爲類變量(static變量)分配內存並設置類變量初始值的階段,這些內存都將在方法去中進行分配
    • 解析:虛擬機常量池的符號引用替換爲字節引用過程
  • 初始化

    • 初始化階段是執行類構造器()方法的過程。類構造器()方法是由編譯器自動收藏類中的所有類變量的賦值動作和靜態語句塊(static塊)中的語句合併產生
    • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
    • 虛擬機會保證一個類的()方法在多線程環境中被正確加鎖和同步
    • 當範圍一個Java類的靜態域時,只有真正聲名這個域的類纔會被初始化

例子:

public class Demo01 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.width);
    }
}

class A{
    public static int width=100; //靜態變量,靜態域 field
    static{
        System.out.println("靜態初始化類A");
        width = 300 ;
    }
    public A() {
        System.out.println("創建A類的對象");
    }
}

這裏寫圖片描述

1、JVM加載Demo01時候,首先在方法區中形成Demo01類對應靜態數據(類變量、類方法、代碼…),同時在堆裏面也會形成java.lang.Class對象(反射對象),代表Demo01類,通過對象可以訪問到類二進制結構。然後加載變量A類信息,同時也會在堆裏面形成a對象,代表A類。

2、main方法執行時會在棧裏面形成main方法棧幀,一個方法對應一個棧幀。如果main方法調用了別的方法,會在棧裏面挨個往裏壓,main方法裏面有個局部變量A類型的a,一開始a值爲null,通過new調用類A的構造器,棧裏面生成A()方法同時堆裏面生成A對象,然後把A對象地址付給棧中的a,此時a擁有A對象地址。

3、當調用A.width時,調用方法區數據。


細節展開

這部分將對,上訴提到的內容進行展開。

運行時數據區

Java堆

在JVM中,堆(heap)是可供各條線程共享的運行時內存區域,也是供所有類實例和數據對象分配內存的區域。
Java堆在JVM啓動的時候就被創建,堆中儲存了各種對象,這些對象被自動管理內存系統(Garbage Collector(我們常提到的GC))所管理。
Java堆的容量可以是固定大小,也可以隨着需求動態擴展,並在不需要過多空間時自動收縮。

Java 堆存在的異常:
OutOfMemoryError:如果實際所需的堆空間超過了自動內存管理系統能提供的最大容量時拋出。


方法區(Method Area)

方法區是可供各條線程共享的運行時內存區域。存儲了每一個類的結構信息,例如:運行時常量池、字段和方法數據、構造函數和普通方法的字節碼內容、還包括一些在類、實例、接口初始化時用到的特殊方法。
方法區在JVM啓動的時候創建。
方法區的容量可以是固定大小的,也可以隨着程序執行的需求動態擴展,並在不需要過多空間時自動收縮。

Java 方法區異常:
OutOfMemoryError: 如果方法區的內存空間不能滿足內存分配請求,那麼JVM將拋出一個OutOfMemoryError異常。

運行時常量池(Runtime Constant Pool):
運行時常量池是每一個類或接口的常量池(Constant_Pool)的運行時表現形式,它包括了若干種常量:編譯器可知的數值字面量到必須運行期解析後才能獲得的方法或字段的引用。
運行時常量池是方法區的一部分。每一個運行時常量池都分配在JVM的方法區中,在類和接口被加載到JVM後,對應的運行時常量池就被創建。
運行時常量池異常:
OutOfMemoryError:當創建類和接口時,如果構造運行時常量池所需的內存空間超過了方法區所能提供的最大內存空間後就會拋出OutOfMemoryError。


Java虛擬機棧

Java虛擬機棧是線程私有的。每一個JVM線程都有自己的Java虛擬機棧,這個棧與線程同時創建,它的生命週期與線程相同。
虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

Java虛擬機棧異常情況:
StackOverflowError:當線程請求分配的棧容量超過JVM允許的最大容量時拋出。
OutOfMemoryError:如果JVM Stack可以動態擴展,但是在嘗試擴展時無法申請到足夠的內存去完成擴展,或者在建立新的線程時沒有足夠的內存去創建對應的虛擬機棧時拋出。


程序計數器

是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的信號指示器。
每一條JVM線程都有自己的程序計數器。
在任意時刻,一條JVM線程只會執行一個方法的代碼。該方法稱爲該線程的當前方法(Current Method)。
如果該方法是java方法,那程序計數器保存JVM正在執行的字節碼指令的地址
如果該方法是native,那程序計數器的值是undefined。
此內存區域是唯一一個在JVM規範中沒有規定任何OutOfMemoryError情況的區域。


類加載器以及雙親委派機制

類加載器

類加載器:在類加載階段,有一步是“通過類的全限定名來獲取描述此類的二進制字節流”,而所謂的類加載器就是實現這個功能的一個代碼模塊,這個動作是在JVM外部實現的,這樣做可以讓應用程序自己決定如何去獲取所需要的類。

作用:首先類加載器可以實現最本質的功能即類的加載動作。同時,它還能夠結合Java類本身來確定該類在JVM中的唯一性。用通俗的話來說就是:比較兩個類是否相等,只有這兩個類是由同一個類加載器加載纔有意義。否則,即使這兩個類是來源於同一個Class文件,只要加載它們的類加載器不同,那麼這兩個類必定不相等。(保證了安全性,避免替換核心類的加載)


雙親委派機制

(以下部分內容來自網絡:http://chenzhou123520.iteye.com/blog/1601319

大部分Java程序一般會使用到以下三種系統提供的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader):負責加載JAVA_HOME\lib目錄中並且能被虛擬機識別的類庫到JVM內存中,如果名稱不符合的類庫即使放在lib目錄中也不會被加載。該類加載器無法被Java程序直接引用。

  • 擴展類加載器(Extension ClassLoader):該加載器主要是負責加載JAVA_HOME\lib\ext目錄中的類庫。該加載器可以被開發者直接使用。

  • 應用程序類加載器(Application ClassLoader):該類加載器也稱爲系統類加載器,它負責加載用戶類路徑(Classpath)上所指定的類庫,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

雙親委派模型:該模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。子類加載器和父類加載器不是以繼承(Inheritance)的關係來實現,而是通過組合(Composition)關係來複用父加載器的代碼。

工作過程
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的加載器都是如此,因此所有的類加載請求都會傳給頂層的啓動類加載器,只有當父加載器反饋自己無法完成該加載請求(該加載器的搜索範圍中沒有找到對應的類)時,子加載器纔會嘗試自己去加載。

使用這種模型來組織類加載器之間的關係的好處是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如java.lang.Object類,無論哪個類加載器去加載該類,最終都是由啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。否則的話,如果不使用該模型的話,如果用戶自定義一個java.lang.Object類且存放在classpath中,那麼系統中將會出現多個Object類,應用程序也會變得很混亂。如果我們自定義一個rt.jar中已有類的同名Java類,會發現JVM可以正常編譯,但該類永遠無法被加載運行。

代碼實現:

protected synchronized Class<?> loadClass(String name, boolean resolve)   throws ClassNotFoundException{  
    // First, check if the class has already been loaded  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        try {  
            if (parent != null) {  
                c = parent.loadClass(name, false);  
            } else {  
                c = findBootstrapClassOrNull(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // ClassNotFoundException thrown if class not found  
            // from the non-null parent class loader  
        }  
        if (c == null) {  
            // If still not found, then invoke findClass in order  
            // to find the class.  
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}  

先檢查是否已經被加載過,如果沒有則調用父加載器的loadClass()方法,如果父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父類加載器加載失敗,則先拋出ClassNotFoundException,然後再調用自己的findClass()方法進行加載。


JVM垃圾回收機制

根搜索算法(GC Roots Tracing)

用於識別

思想:通過一系列的稱爲“GC Roots”的點作爲起始進行向下搜索,當從GC到某一個對象不可達的時候,也就是說一個對象到GC Roots沒有任何引用鏈相連的時候,這個對象就會被判定爲可回收的。

在java中,可作爲GC Roots的對象包括下面幾種:

  • 1.虛擬機棧(棧幀中的本地變量表)中的引用的對象。
  • 2.方法區中的類靜態屬性引用的對象。
  • 3.方法區中的常量引用的對象。
  • 4.本地方法棧中JNI(即一般說的Native方法)引用的對象。

用於回收的算法

標記-清除算法:

標記階段:從GC Roots開始進行可達性分析,將所有可達對象標記出來
清除階段:將所有未標記可達的對象清理掉
標記-清除算法帶來的問題是內存碎片,被清除掉的對象會在內存中留下很多的“洞”,這些“洞”的大小可能不足以容下新創建的對象,隨着碎片的增多,內存池中實際可用的內存可能會變得越來越少。

複製算法:

爲了解決效率問題,有了“複製”的算法,他將可用內存分爲大小相同兩塊。每次只用一塊,當一塊空間用完了,就將還存活的對象複製到另一塊上,然後將剛使用過的內存空間一次清理掉。這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況。實現簡單,運行高效。只是這種算法的代價是將內存縮小到原來的一半。

一般來說:將內存分爲一塊較大的Eden空間和兩塊較小的Surivior空間,每次使用Eden空間和其中一塊Surivior空間。當回收時,將Eden和Surivior中還存活的對象一次性的拷貝到另一塊Surivior中,最後清理掉Eden和剛用過的Survivor空間。

標記-整理算法:

與標記-清除算法類似,區別在於在標記階段完成後,將所有可達對象移動到內存池的另一端,形成一整塊連續的內存空間,然後再將範圍外的內存池清空。
標記-壓縮算法的好處是不會產生內存碎片,也不會佔用冗餘的內存空間,但代價是需要付出額外的工作將存活對象移動,效率低於標記-清除算法。

分代收集算法

一般是把Java堆分作新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就用複製算法,只要少量複製成本就可以完成收集。而老年代中因爲對象的存活率較高、週期長,就用“標記-整理”或“標記-清除”算法來回收。

簡單來說,我們在對處理年輕代的對象時,使用的是複製算法,也就是8:1:1的比例劃分出了Eden,Surivior(from),Surivior(to)。每次回收,會使用一塊Eden,和一塊Surivior,回收不掉的移入另一塊Surivior。多次沒有被回收將移入老年代。
對於老年代將採取標記-整理算法進行回收。

新生代使用複製算法,當Minor GC時如果存活對象過多,無法完全放入Survivor區,就會向老年代借用內存存放對象,以完成Minor GC。

在觸發Minor GC時,虛擬機會先檢測之前GC時租借的老年代內存的平均大小是否大於老年代的剩餘內存,如果大於,則將Minor GC變爲一次Full GC,如果小於,則查看虛擬機是否允許擔保失敗,如果允許擔保失敗,則只執行一次Minor GC,否則也要將Minor GC變爲一次Full GC。


尾聲

OK,JVM的部分到此就算是記錄完畢。

最後希望各位看官可以star我的GitHub,三叩九拜,滿地打滾求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp

發佈了106 篇原創文章 · 獲贊 74 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章