Java 特性學習之虛擬機 (JVM)

概念:Java虛擬機(Java  Virtual  Machine 簡稱 JVM)是運行所有Java程序的抽象計算機,是Java語言的運行環境,也是java的重要特性之一。

介紹:Java (JVM)一種用於計算機設備的規範,可用不同的方式(軟件或硬件)加以實現。編譯虛擬機的指令集與編譯微處理器的指令集非常類似。JVM虛擬機包括一套字節碼指令集、一組存儲器、一個棧、一個垃圾回收堆、一個存儲方法域。

           JVM也可以理解爲一臺可運行Java程序的假象計算機。只要根據JVM的規則描述將解釋器移植到特定的計算機上,就能夠保證經過編譯的任何Java代碼能夠在該程序上運行。通俗點說就是Java 程序能夠運行在任意一臺運行了JVM的平臺,也體現了Java語言的一處編譯,隨處運行的語言特性。JVM可以以一次一條指令的方式來解釋字節碼(把它映射到實際的處理器指令),或者字節碼也可以由實際處理器中稱作just-in-time的編譯器進行進一步編譯。

特點:上邊介紹了Java的“一處編譯,隨處運行”,可見Java語言的與平臺的無關性。而這一特點的關鍵就在“一處編譯”這一環節,也就是JVM(Java虛擬機)。對於要在特定平臺上,JVM 只需要把字節碼解釋成具體平臺上的機器指令去執行就可以了。

應用:Java虛擬機(JVM)是Java語言底層實現的基礎,這有助於理解Java語言的一些性質,也有助於使用Java語言。對於要在特定平臺上實現Java虛擬機的軟件人員,Java語言的編譯器作者以及藥用硬件芯片實現Java虛擬機的人來說,則必須深刻理解Java虛擬機的規範。如果想擴展Java語言,或是把其他語言編譯成Java的字節碼就需要深入瞭解Java的JVM.

一、Java虛擬機的內存佈局;

       JVM虛擬機在執行Java程序過程中會在內存空間中分配一片區域,用於程序的運行。

         

                                                                             圖  1-1

       如上圖;虛擬機又會把這塊管理的內存劃分爲若干個不同的數據區域,即虛擬機棧、本地方法區、程序計數器、堆、方法區。左側三個爲線程私有區域,右側兩個爲線程共享區域。

       1、程序計數器:當線程所執行的字節碼的行號指示器。

        字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等都依賴於這個計數器。

        在程序開始執行前,將程序指令序列的起始地址,即程序的第一條指令所在的內存單元地址送入PC,CPU此時通過PC的指示從內存讀取第一條指令(取指),Java的入口就是main方法。  

          

        2、虛擬機棧:Java方法的執行模型,用於存儲局部變量表,操作數棧、動態鏈接、方法出口等。

        局部變量存放了編譯器可知的各種類型,對象引用,returnAddress類型。

        局部變量表所需的內存空間在編譯期間完成分配。。

        如果線程請求的棧深度,大於虛擬機所允許的深度,將拋出StackoverflowError;

        如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryErrorr;

       3、本地方法棧:執行native方法,也會拋出兩種異常,有的虛擬機將本地方法棧與虛擬機棧合二爲一。

       4、堆:存放對象實例,可以處於物理上不連續的內存空間中,只要邏輯上連續即可。像磁盤的空間一樣,可以擴展,如果沒有內存完成實例分配,並且堆也無法再擴展,將會拋出OutOfMemoryError異常。

      5、方法區:存儲已被虛擬加載的類信息,常量,靜態變量,即時編譯後的代碼等數據。方法區無法滿足內存分配需求時,拋出OutOfMemoryError異常。

      6、直接內存:虛擬機所控制之外的內存空間,NIO可以通過native方法庫直接 分配內存,擴展時也會出現OutOfMemoryError 異常。

二、JVM體系總體分爲四大塊;

        2.1、類的加載機制

                 全盤負責,當一個類加載器負責加載某個Class時,該Class依賴的和引用的其他Class也將由該類加載器負責                載入,除非顯示使用另外一個加載器來載入

                 父類委託,先讓父類加載器加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。

                 緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序需要某個Class時,類加載器先從緩存區              尋找該Class,只有緩存區不存在,系統才能讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區,這就是修        改了Class之後必須重啓JVM,程序的修改纔會生效。

        2.2、JVM內存結構

                 如圖1-1

        2.3、GC算法,內存回收

                2.3.1、內存分配與回收策略

                            先看下圖;

                        

        圖中所示爲堆中內存分配示意圖,當程序創建一個對象首先會在eden當中分配一塊區域,如果內存不夠,就    會將年齡較大的對象轉移到survivor 區域,當該區域存放不下則會將對象轉入老年代區域。對於一些靜態變量不    需要使用對象,直接被調用的則會被放入永生代(即非堆內存),一般來說長期存活的對象最終會被放入年老        代 ,或者創建大對象,比如數據之類的需要申請聯繫空間,且空間較大的,則直接放入到年老代。

        在回收過程中,有個描述對象年齡的參數,如果在一次垃圾回收過程中,有使用過該對象則系統對該對象年齡參數+1,否則-1,當計數至0時,則進行垃圾回收。如果年齡達到一定峯值,則對象進入老年代。總的來說內存分配機制只要體現在創建的對象是否還在使用,不使用則回收,使用則對年齡進行更新,最終將放入年老代。

                 2.3.2、垃圾回收算法

                             2.3.2.1、標記-清除算法

           該算法先標記後清除,將所有需要回收的算法進行標記,然後清除,這種算法的缺點是;效率    低,標記清除後會出現大量連續的內存碎片,這些碎片太多會使存儲對象出發GC回收,造成內存浪  以及時間的消耗。

                             2.3.2.2、複製算法

           複製算法將可用的內存分爲兩份,每次使用其中一份,當這塊回收之後把未回收的複製到另一    塊內存中,然後把使用的清除。這種算法運行簡單,解決了標記清除算法的碎片問題,但是這種算    法的代價過高,需要將可用的內存縮小一半,對象存活率較高時,需要持續的複製工作,效率低。

                             2.3.2.3、標記整理 算法

           標記整理算法是針對複製算法在對象存活率較高時而需要進行持續復製造成的效率低的問題的    改進,該算法是在標記清除算法的基礎上不直接清理,而是使存貨對象往一端遊走,然後清除一端    邊界以外的內存,這樣既可以避免不連續空間出現,還可以避免對象成活率較高的持續複製。這種    算法可以百分百避免對象存活的極端狀況,因此老年代不能直接使用該算法。

                              2.3.2.4、分代收集算法 

            分代回收算法就是虛擬機目前使用的回收算法,它解決了標記整理不適用年老代的問題,將內    存分爲各個年代,  在不同年代使用合適的算法,新生代存活率低,可以是引用複製算法。而年老代  存活率較高,沒有額外的空間對其進行分配擔保,所以只能使用標記清除或者標記整理算法。      

        2.4、GC分析  命令調優

                GC調優的概念:在軟件工程領域,如果希望提高一個系統的吞吐量,可以着手從延遲,吞吐量兩方面來考慮,要麼投入更多的硬件成本,要麼提高系統性能,縮短延遲時間。希望對給定的硬件環境充分利用,在給定  的硬件環境基礎上實現最優性能。

               2.4.1、延遲(Latency)

               2.4.2、吞吐量(Throughput)

               2.4.3、系統容量(Capacity)

               根據上述三個性能方面的衡量維度

三、類的加載機制;

       3.1、什麼是類的加載

                將Java類加載到JVM的東西,我們稱之爲類加載器。JVM使用類的方式如下;Java源程序(.java文件),在經過Java          編譯器編譯之後就被轉換成Java字節代碼(.class文件)。類加載器負責讀取Java字節代碼,並轉換成java.lang.Class類的          一個實例。每個這樣的實例表示一個Java類。通過此實例的 newlnstance()方法就可以創建出該類的一個對象。

       3.2、Java類加載的時機

                3.2.1、類加載的生命週期

                 類加載的生命週期是指類從被加載到內存開始,直至卸載出內存爲止的這一過程。整個週期分爲;加載、          驗證、準備、解析、初始化、使用、卸載。其中驗證、準備、解析稱爲連接。如下示意圖;

                     

                              下面簡單介紹類加載器所執行的生命週期的過程;

                                     (1)、裝載:查找和導入Class文件;

                                     (2)、鏈接:把類的二進制數據合併到JRE中;      

                                                  1、校驗:檢查載入Class文件數據的正確性。

                                                  2、準備:給類的靜態變量分配存儲空間。

                                                  3、解析:將符號引用轉成直接引用。

                                      (3)、初始化:對類的靜態變量,靜態代碼塊執行初始化操作。

                3.2.2、類加載的時機

                            類加載的時機JVM使用規範中並沒有強制規定,但是在一下五個場景必須立即執行初始化,被稱作主動引用

                            (1) 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸 發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候,讀取或設      置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一      個類的靜態方法的時候。

                            (2)、使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初    始化。

                            (3)、當初始化一個類的時候,發現其父類還沒有進行過初始化,則要先觸發其父類的初始化。

                            (4)、當JVM啓動時,用戶需要指定一個要執行的主類(包含main方法的那個類),虛擬機會先初始化這個主    類。

                            (5)、當使用JDK 1.7動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果                  REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且該方法句柄所對應的類沒有初始      化過,則先觸發初始化。

                3.2.3、Java類加載的過程

                           3.2.3.1、加載

                                         (1)、通過一個類的權限定名來獲取定義此類的二進制字節流;

                                         (2)、將這個字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構。

                                         (3)、在Java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區這些數據的訪問入口。

                           3.2.3.2、驗證

                                                 驗證階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危  害JVM自身的安全。會對四個方面進行驗證;

                                         (1)、文件格式驗證

                                         (2)、元數據驗證

                                         (3)、字節碼驗證

                                         (4)、符號引用驗證

                           3.2.3.3、準備

                                                準備階段是正是爲類變量分配並設置類變量初始值的階段,這些內存都將在方法區中進行分配。    注:這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量會      在對象實例化時隨着對象一起分配在Java堆中,這裏所說的初始值通常情況下是數據類型的零值。

                           3.2.3.4、解析

                                                 解析階段是JVM將常量池中的符號引用替換成直接引用的過程。直接引用是直接指向目標的指      針相對偏移量或是一個能間接定位到目標的句柄。直接引用與JVM 實現的內存有關,同一個符號引      用在不同JVM的實例上翻譯出來的直接引用不盡相同。

                           3.2.3.5、初始化

                                                 初始化階段是類加載過程的最後一步,到了該階段才真正開始執行類定義的Java程序代碼,根據程序員通過代碼定製的主觀計劃去初始化類變量和其他資源,是執行類構造器初始化方法的過程。

四、類加載器

              類加載器負責加載所有的類,爲所有被載入到內存中的類生成一個java.lang.Class中的一個實例對象。如果一個類被加載到JVM中,同一個類就不會被再次載入了,正如一個對象只有一個唯一標識一樣,一個被載入到JVM的類也會有 一個唯一標識。在Java中(即未被編譯的.java文件)一個類用全限定類名(類名+包名)作爲標識,但是在JVM 中(即經過JVM 編譯過的.class文件),一個類用其權限定名和類加載器作爲唯一標識。

               JVM預定義有三種類加載器,當一個JVM啓動的時候,Java開始使用以下三種類加載器:

               (1)、根類加載器(bootstrap  class loader):用來加載Java的核心類,此加載類爲C++實現,所以並不繼承自java.lang.Classloader,換句話說與其他加載器不同,根類加載器並不是java.lang.Classloader的子類。

               (2)、擴展類加載器(extensions class loader):負責加載JRE的擴展目錄,lib/ext或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類,由java語言實現,福類加載器爲null.

               (3)、系統類加載器(system class loader ):被稱爲系統類加載器(也稱爲應用類加載器),它負責JVM啓動時加載來自Java命名的 -classpath選項,java.class.path系統屬性。系統可以通過ClassLoader的靜態方法getSystemClassLoader()來獲取類加載器,由Java語言實現,父類記載器爲ExtClassLoader。

       類加載器加載Class大致需要8個步驟;

              1、檢測此Class是否被載入過,即在緩存區是否有次Class,如果有直接進入第8步,否在第2步。

              2、如果沒有父類加載器,則要麼Parent是根類加載器,要麼本身就是根類加載器,則跳入第4步,如果父類記載器存在則第3步。

              3、請求使用父類加載器載入目標類,如果載入成功則跳入第8步,否則接着執行第5步。

              4、請求使用根類加載器載入目標類,如果成功則跳入第8步,否則第7步。

              5、當前類的加載器嘗試尋找CLass文件,如果找到則執行第 6步,否則執行第7步。

              6、從文件中載入Class,成功則跳至第8步。

              7、拋出ClassNotFountException異常。

              8、返回對應的java.lang.Class對象。

五、Java引用的四種狀態;

              1、強引用:應用最廣泛,平時 new 一個對象放在堆內存,然後用一個引用指向它,這就是強引用。

              如果一個對象具有強引用,那垃圾回收機制不會回收它。當內存不足,Java虛擬機寧願出OutOfMemoryError異常,是程序終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。

              2、軟引用:如果一個對象具有軟引用,則內存空間足夠時,垃圾回收器就不會回收它,但是內存如果不足,則隨時都有可能被回收。只要未被回收,則程序可以繼續使用該對象。軟引用用來實現敏感的高速緩存。

              3、弱引用:與軟引用類似,區別在於擁有弱引用的生命週期更爲短暫,每次執行GC的時候,一旦發現有弱引用的對      象,不論當前內存空間足夠與否,都會回收它的內存。但是,垃圾回收器是一個優先級和低的線程。 

              4、虛引用:與其他幾種引用不盡相同,虛引用不會決定對象的生命週期。如果一個對象具有虛引用,那麼它和沒有任    何引用一樣,在任何時候都可能被垃圾回收器回收。

六、結尾,下面通過一張圖對本文的技術點進行一個直觀的展示(圖片來源於網絡)

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章