Java虛擬機一:深入理解JVM內存模型

什麼是JVM

JVM是Java Virtual Machine(Java 虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。Java語言的一個非常重要的特點就是平臺無關性。而使用Java虛擬機是實現這一特點的關鍵。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息Java虛擬機與操作系統進行交互,操作系統與硬件進行交互。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。這就是Java的能夠“一次編譯,到處運行”的原因。

JVM基本構成

ä¸æ带你深å¥ç解JVM

JVM總體上是由類裝載器(ClassLoader)運行時數據區執行引擎垃圾收集這四個部分組成。其中我們最爲關注的運行時數據區,也就是JVM的內存部分則是由方法區(Method Area)、JAVA堆(Java Heap)、虛擬機棧(JVM Stack)、程序計數器、本地方法棧(Native Method Stack)這幾部分組成。

1.類裝載器:在jvm啓動時或者類運行時,將需要的class文件(字節碼文件)加載到JVM中。

Java編譯器(Java Compiler)負責將Java Source(.java File)編譯成字節碼文件(.class File),類加載器將class文件加載到JVM中。

2.運行時數據區(內存區):是JVM運行的時候操作所分配的內存區。運行時內存區主要可以劃分爲5個區域

å¨è¿éæå¥å¾çæè¿°

方法區(Method Area) 

方法區是各線程共享的內存區域,用於存儲類結構信息的地方包括所有的class和Static變量,即常量、靜態變量、構造函數等以及編譯後的方法實現的二進制形式的機器指令集等數據。其中方法區還包含運行時常量池。可以選擇不實現垃圾收集。被裝載的class的信息存儲在Methodarea的內存中。

②Java堆(Heap)

Java堆是各線程共享的內存區域,用於存貯Java對象和數組的地方,是GC主要的回收區,堆內存分爲三部分:新生代、老年代、永久代。Jdk1.8及之後:無永久代,改用元空間代替(java.lang.OutOfMemoryError: PermGen space,這種錯誤將不會出現在JDK1.8中)。

Java棧(JVM Stack)

Java棧是線程私有的,java棧總和是線程關聯在一起,線程結束棧內存也就釋放。每當創建一個線程時,jvm會爲這個線程創建一個對應的java棧,在這個java棧中又會包含過個棧幀,每運行一個方法就創建一個棧幀,用於存貯局部變量表、操作棧等。每一個方法從調用直至執行完成的過程,就對應一個棧幀在java棧中入棧到出棧的過程。

java棧中存放局部變量的基本數據類型和對象的引用。

本地方法棧(Native Method Stack)

和java棧的作用差不多,只不過是爲jvm使用到的本地方法服務的。

 程序計數器(Program Counter Register)

程序計數器是一塊非常小的內存空間,幾乎可以忽略不計,用於保存當前線程執行的內存地址,由於jvm程序是多線程執行的(線程輪休切換),所以爲了保證線程切換回來後,還能恢復到原先狀態,就需要每個線程有一個獨立的計數器,記錄之前終端的地方。

3.執行引擎:負責執行class文件中包含的字節碼指令,執行引擎以指令爲單位讀取Java字節碼。它就像一個CPU一樣,一條一條地執行機器指令。每個字節碼指令都由一個1字節的操作碼和附加的操作數組成。執行引擎取得一個操作碼,然後根據操作數來執行任務,完成後就繼續執行下一條操作碼。

不過Java字節碼是用一種人類可以讀懂的語言編寫的,而不是用機器可以直接執行的語言。因此,執行引擎必須把字節碼轉換成可以直接被JVM執行的語言。字節碼可以通過以下兩種方式轉換成合適的語言:

  • 解釋器: 一條一條地讀取,解釋並執行字節碼執行,所以它可以很快地解釋字節碼,但是執行起來會比較慢。這是解釋執行語言的一個缺點。
  • 即時編譯器:用來彌補解釋器的缺點,執行引擎首先按照解釋執行的方式來執行,然後在合適的時候,即時編譯器把整段字節碼編譯成本地代碼。然後,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過本地代碼去執行。執行本地代碼比一條一條進行解釋執行的速度快很多,編譯後的代碼可以執行的很快,因爲本地代碼是保存在緩存裏的。

4.垃圾收集(Garbage Collection, GC)

垃圾收集即垃圾回收,簡單的說垃圾回收就是回收內存中不再使用的對象。所謂使用中的對象(已引用對象),指的是程序中有指針指向的對象;而未使用中的對象(未引用對象),則沒有被任何指針給指向,因此佔用的內存也可以被回收掉。

垃圾收集器一般必須完成兩件事:檢測出垃圾和回收垃圾

檢測垃圾方法:

①引用計數法:給一個對象添加引用計數器,每當用地方引用他,計數器就+1,引用失效就-1.但是有個問題,如果有兩個對象A和B,互相引用,除此之外沒有其他任何對象引用他們,實際上這兩個對象已經無法訪問,即是我們說的垃圾對象。但是相互引用,計數不爲0,導致無法回收。

②可達性分析法:以根集對象爲起點進行搜索,如果有對象不可達的話,即是垃圾對象。這裏的根據一般包括 Java棧中引用的對象,方法區常量池中引用的對象、本地方法中引用的對象等。總之,jvm在做垃圾回收的時候,會檢查堆中的所有對象是否會被這些根集對象引用,不能夠被引用的對象就會被垃圾收集器收集。

可達性:在java中對象是通過引用使用的,如果沒有引用指向該對象,那麼該對象將無從處理或使用,這樣的對象爲不可達。

垃圾回收算法:

①標記清除算法

標記-清楚算法採用從根集合(GC Roots)進行掃描,首先標記出所有需要回收的對象(根搜索算法),標記完成後統一回收掉所有被標記的對象。

ä¸æ带你深å¥ç解JVM

存在問題:

  • 效率問題:標記和清除過程的效率都不高;
  • 空間問題:標記清除後會產生大量不連續的內存碎片, 空間碎片太多可能會導致在運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集。

複製算法

複製算法是將可用內存按容量劃分爲大小相等的兩塊, 每次只用其中一塊, 當這一塊的內存用完, 就將還存活的對象複製到另外一塊上面, 然後把已使用過的內存空間一次清理掉。

標記-整理算法(Mark-Compact)

標記整理算法的標記過程與標記清除算法相同, 但後續步驟不再對可回收對象直接清理, 而是讓所有存活的對象都向一端移動,然後清理掉端邊界以外的內存。

ä¸æ带你深å¥ç解JVM

分代收集算法(Generational Collection)

分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不同的區域。一般情況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),在堆區之外還有一個代就是永久代(Permanet Generation)。老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。

ä¸æ带你深å¥ç解JVM

垃圾收集器

(1)Serial收集器【串行收集器】:新生代單線程收集器
(2)ParNew收集器【串行收集器的多線程版本】:新生代多線程收集器,其實就是Serial收集器的多線程版本
(3)Parallel Scavenge收集器【PS收集器】

  新生代並行的多線程收集器,追求高吞吐量,高效利用CPU。吞吐量一般爲99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC      線程時間)。適合後臺應用等對交互相應要求不高的場景。是server級別默認採用的GC方式,可用-XX:+UseParallelGC來強制      指定,用-XX:ParallelGCThreads=4來指定線程數。
(4)CMS收集器【老年代收集器】:CMS收集器是一種以獲取最短回收停頓時間爲目標的收集器
(5)G1收集器(Garbage First) G1是一款面向服務端應用的垃圾收集器

ä¸æ带你深å¥ç解JVM

以上JVM基本內存結構就講解完了,針對線程棧進行重點說明。

Java虛擬機棧(Java Virtual Machine Stack)

棧運行原理:棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀F1,並被壓入到棧中,A方法又調用了B方法,於是產生棧幀F2也被壓入棧,B方法又調用了C方法,於是產生棧幀F3也被壓入棧…… 依次執行完畢後,先彈出後進......F3棧幀,再彈出F2棧幀,再彈出F1棧幀。

Java虛擬機對運行時虛擬機棧(JVM Stack)的組織

Java虛擬機在運行時會爲每一個線程在內存中分配了一個虛擬機棧,來表示線程的運行狀態和信息,虛擬機棧中的元素稱之爲棧幀(JVM stack frame),每一個棧幀表示這對一個方法的調用信息。如下所示:

類加載器將class文件加載到JVM,將類結構信息和編譯後的方法實現的二進制形式的機器指令集等數據放進方法區,動態鏈接通俗講運行時動態的從方法區獲取字節碼機器指令集數據。棧幀中保存了一個引用,指向該方法在運行時常量池中的位置。

操作數棧:臨時的內存空間,對相關變量指令進行計算使用,將結果存放在局部變量表。

定義一個public class Bootstrap類,方法實現如下:

爲main方法創建棧幀:爲main方法創建一個棧幀(VM Stack),並將其加入虛擬機棧中

舉例說明:


public class Demo {
 
    public static void foo() {
       int a = 1;
       int b = 2;
       int c = (a + b) * 5;
    }
}

簡單解釋下執行過程,注意:偏移量的數字只是簡單代表第幾個指令哦,首先常數1入棧,棧頂元素就是1,然後棧頂元素移入局部變量區存儲,常數2入棧,棧頂元素變爲2,然後棧頂元素移入局部變量區存儲;接着1,2依次再次入棧,彈出棧頂兩個元素相加後結果入棧,將5入棧,棧頂兩個元素彈出並相乘後結果入棧,然後棧頂變爲15,最後移入局部變量。執行return命令如果當前線程對應的棧中沒有了棧幀,這個Java棧也將會被JVM撤銷。示意圖如下:

 

文章參考:

https://blog.csdn.net/wangtaomtk/article/details/52267634

https://blog.csdn.net/csdnliuxin123524/article/details/81303711

https://www.toutiao.com/a6717906477710836236/

 

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