JVM內存結構和Java內存模型(JMM)

Java程序具體執行的過程

在討論JVM內存區域劃分之前,先來看一下Java程序具體執行的過程:
在這裏插入圖片描述
Java源代碼文件(.java後綴)首先會被Java編譯器編譯爲字節碼文件(.class後綴),然後JVM中的類加載器加載各個類的字節碼文件,加載完畢之後,交由JVM執行引擎執行。在整個程序執行過程中,JVM會用一段空間來存儲程序執行期間需要用到的數據和相關信息,這段空間一般被稱作爲Runtime Data Area(運行時數據區),也就是我們常說的JVM內存。因此,在Java中我們常常說到的內存管理就是針對這段空間進行管理(如何分配和回收內存空間)。

JVM內存結構

在這裏插入圖片描述

1.程序計數器

是說是用來指示執行哪條指令的。由於在JVM中,多線程是通過線程輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,因此,爲了能夠使得每個線程都在線程切換後能夠恢復在切換之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,並且不能互相被幹擾,否則就會影響到程序的正常執行次序。因此,可以這麼說,程序計數器是每個線程所私有的。

在JVM規範中規定,如果線程執行的是非native方法,則程序計數器中保存的是當前需要執行的指令的地址;如果線程執行的是native方法,則程序計數器中的值是undefined。

由於程序計數器中存儲的數據所佔空間的大小不會隨程序的執行而發生改變,因此,對於程序計數器是不會發生內存溢出現象(OutOfMemory)的。

我們學過多線程,有兩個線程,其中一個線程可以暫停使用,讓其他線程運行,然後等自己獲得cpu資源時,又能從暫停的地方開始運行,那麼爲什麼能夠記住暫停的位置的,這就依靠了程序計數器。

2.虛擬機棧

虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用來存放存儲局部變量表、操作數表、動態連接、方法出口等信息,每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。比如執行一個類(類中有main方法)時,執行到main方法,就會把爲main方法創建一個棧幀,然後在加到虛擬機棧中,棧幀中會存放這main方法中的各種局部變量,對象引用等東西。如圖
在這裏插入圖片描述

當在main方法中調用別的方法時,就會有另一個方法的棧幀入虛擬機棧,當該方法調用完了之後,彈棧,然後main方法處於棧頂,就繼續執行,直到結束,然後main方法棧幀也彈棧,程序就結束了。總之虛擬機棧中就是有很多個棧幀的入棧出棧,棧幀中存放的都市一些變量名等東西,所以我們平常說棧中存放的是一些局部變量,因爲局部變量就是在方法中。也就是在棧幀中,就是這樣說過來的。

3.本地方法棧

本地方法棧則是爲執行本地方法(Native Method)服務的。它和虛擬機棧的作用和原理類似。區別只不過是虛擬機棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。

以上說的三個都是線程不共享的,也就是這部分內存,每個線程獨有,不會讓別的線程訪問到,接下來的兩個就是線程共享了,也就會出現線程安全問題。

4.堆

Java虛擬機所管理的內存中最大的一塊,因爲該內存區域的唯一目的就是存放對象實例。幾乎所有的對象實例度在這裏分配內存,也就是通常我們說的new對象,該對象就會在堆中開闢一塊內存來存放對象中的一些信息,比如屬性呀什麼的。同時堆也是垃圾收集器管理的主要區域。因此這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被所有線程共享的,在JVM中只有一個堆。
從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集算法,所以Java堆還可以細分爲:新生代和老年代:其中新生代又分爲:Eden空間、From Survivor、To Survivor空間。進一步劃分的目的是更好地回收內存,或者更快地分配內存。“分代回收”是基於這樣一個事實:對象的生命週期不同,所以針對不同生命週期的對象可以採取不同的回收方式,以便提高回收效率。從內存分配的角度來看,線程共享的java堆中可能會劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。

在這裏插入圖片描述

如圖所示,JVM內存主要由新生代、老年代、永久代構成。

① 新生代(Young Generation):大多數對象在新生代中被創建,其中很多對象的生命週期很短。每次新生代的垃圾回收(又稱Minor GC)後只有少量對象存活,所以選用複製算法,只需要少量的複製成本就可以完成回收。

新生代內又分三個區:一個Eden區,兩個Survivor區(一般而言),大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到From Survivor區。當From Survivor區滿時,此區的存活且不滿足“晉升”條件的對象將被複制到To Survivor區(注意:From Survivor 和 To Survivor會相互轉換)。對象每經歷一次Minor GC,年齡加1,達到“晉升年齡閾值”(默認是15)後,被放到老年代,這個過程也稱爲“晉升”。顯然,“晉升年齡閾值”的大小直接影響着對象在新生代中的停留時間,在Serial和ParNew GC兩種回收器中,“晉升年齡閾值”通過參數MaxTenuringThreshold設定,默認值爲15。

② 老年代(Old Generation):在新生代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代,該區域中對象存活率高。老年代的垃圾回收(又稱Major GC)通常使用“標記-清理”或“標記-整理”算法。整堆包括新生代和老年代的垃圾回收稱爲Full GC(HotSpot VM裏,除了CMS之外,其它能收集老年代的GC都會同時收集整個GC堆,包括新生代)。

③ 永久代(Perm Generation):主要存放元數據,例如Class、Method的元信息,與垃圾回收要回收的Java對象關係不大。相對於新生代和年老代來說,該區域的劃分對垃圾回收影響比較小。

在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。

5.方法區

方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。

在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。

運行時常量池

在方法區中有一個非常重要的部分就是運行時常量池,它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM後,對應的運行時常量池就被創建出來。當然並非Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,比如String的intern方法。
JDK1.7及之後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。同時在 jdk 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域

6.直接內存

直接內存並不是虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致OutOfMemoryError異常出現。

JDK1.4中新加入的NIO(New Input/Output)類,引入了一種基於通道(Channel) 與緩存區(Buffer) 的I/O方式,它可以直接使用Native函數庫直接分配堆外內存,然後通過一個存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆之間來回複製數據。

本機直接內存的分配不會收到Java堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

Java內存模型

Java內存模型(Java Memory Model,JMM)JMM主要是爲了規定了線程和內存之間的一些關係。根據JMM的設計,系統存在一個主內存(Main Memory),Java中所有變量都儲存在主存中,對於所有線程都是共享的。每條線程都有自己的工作內存(Working Memory),工作內存中保存的是主存中某些變量的拷貝,線程對所有變量的操作都是在工作內存中進行,線程之間無法相互直接訪問,變量傳遞均需要通過主存完成。

內存模型解決併發問題主要採用兩種方式: 限制處理器優化和使用內存屏障。

在這裏插入圖片描述
另外,線程在棧區,不能共享數據,只能通過複製共享區的數據作爲一塊緩存,所有多線程寫會有bug,voliate使得取到的數據不做緩存,是實時更新的。關鍵字 volatile 是輕量級的同步機制。
Volatile 變量對於all線程的可見性,指當一條線程修改了這個變量的值,新值對於其他 線程來說是可見的、立即得知的。 Volatile 變量在多線程下不一定安全,因爲他只有可見性、有序性,但是沒有原子性

對象創建過程

在這裏插入圖片描述

1、類加載檢查
  虛擬機遇到一條new指令時,首先將去檢查這個指令的參數能否在常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否已經被加載、解析和初始化過,如果沒有,那麼必須先執行相應的類加載過程。

2、分配內存
  在類加載檢查通過後,接下來虛擬機將會爲新生的對象分配內存。對象所需要的內存大小在類加載完成後便可完全確定,爲對象分配空間等同於把一塊確定大小的內存從java堆中劃分出來。分配方式有 “指針碰撞” 和 “空閒列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

  • (1)指針碰撞法
    假設Java堆中內存是完整的,已分配的內存和空閒內存分別在不同的一側,通過一個指針作爲分界點,需要分配內存時,僅僅需要把指針往空閒的一端移動與對象大小相等的距離。使用的GC收集器:Serial、ParNew,適用堆內存規整(即沒有內存碎片)的情況下。

  • (2)空閒列表法
    事實上,Java堆的內存並不是完整的,已分配的內存和空閒內存相互交錯,JVM通過維護一個空閒列表,記錄可用的內存塊信息,當分配操作發生時,從列表中找到一個足夠大的內存塊分配給對象實例,並更新列表上的記錄。使用的GC收集器:CMS,適用堆內存不規整的情況下。

Java 堆內存是否規整,取決於 GC 收集器的算法是”標記-清除”,還是”標記-整理”(也稱作”標記-壓縮”),值得注意的是,複製算法內存也是規整的。在使用Serial、ParNew等待整理過程的收集器時,採用的是指針碰撞,在使用CMS這種mark-sweep算法的收集器時,使用的是空閒列表。

內存分配併發問題

在創建對象的時候有一個很重要的問題,就是線程安全,因爲在實際開發過程中,創建對象是很頻繁的事情,例如正在給A對象分配內存,但是指針還沒修改,這時候對象B可能使用原來的指針來分配內存的情況。作爲虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機採用兩種方式來保證線程安全:

  • CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操作的原子性。
  • TLAB: 爲每一個線程預先在 Eden 區分配一塊內存。JVM 在給線程中的對象分配內存時,首先在各個線程的TLAB 分配,當對象大於TLAB 中的剩餘內存或 TLAB 的內存已用盡時,再採用上述的 CAS 進行內存分配。虛擬機是否啓用TLAB,可以通過-XX:+/-UseTLAB參數來設定。

3、初始零值
  內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。如果使用TLAB,這一工作過程也可以提前到TLAB分配時進行。

4、設置對象頭
  接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例,如何才能找到類的元數據信息,對象的哈希嗎,對象的GC分代年齡等信息,這些信息存放在對象的對象頭中。根據虛擬機當前的運行狀態的不同,對象頭會有不同的設置方式。

5、執行init方法
  在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建纔剛開始,init 方法還沒有執行,所有的字段都還爲零。所以一般來說,執行 new 指令之後會接着執行 init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。

對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲的佈局分爲3個區域,如下圖所示:
在這裏插入圖片描述

對象頭(Header):

  • MarkWord:存儲對象自身的運行時數據,例如:哈希碼HashCode、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID等。考慮空間效率,MarkWord設計爲非固定的數據結構,它根據對象的不同狀態複用自己的空間,如下圖所示:   
    在32位系統下,對象頭8字節,64位則是16個字節【未開啓壓縮指針,開啓後12字節】在這裏插入圖片描述

  • 指向Class的指針:即對象指向它的類的元數據的指針,虛擬機通過這個指針來確定是哪個類的實例

  • 如果對象是Java數組,對象頭中還需要一塊記錄數組長度的數據

  • 實例數據(Instance Data):對象真正存儲的有效信息,也是程序代碼中定義的各種類型字段的內容

  • 對齊填充(Padding):起佔位符的作用。因爲HotSpot VM的要求對象起始地址必須是8字節的整數倍,也就是對象的大小必須是8字節的整數倍。當對象實例數據部分沒有對齊時,需要對齊填充來補充

參考:

https://blog.csdn.net/kagurawill/article/details/86644212
https://www.cnblogs.com/aiqiqi/p/10770864.html
https://www.cnblogs.com/butterfly100/p/9175673.html

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