JVM 的內存佈局/內存模型

JVM 的內存佈局/內存模型 

更直觀的表示爲:

java虛擬機總共分爲五個區域,其中三個是線程私有:程序計數器,虛擬機棧,本地方法棧,兩個是線程共享:堆,方法區。線程私有的區域等到線程結束時(棧幀出棧時)會自動被釋放,空間比較容易清理。而線程共享的java堆和方法區中的空間較大而且沒有線程的回收容易產生很多垃圾信息,GC垃圾回收真正關心的就是這部分。

下面詳細介紹各個區:

1JAVA堆

堆區是JVM中最大一塊內存區域,存儲着各類生成的對象、數組等,所有通過new創建的對象的內存都在堆中分配,JVM8中把運行時常量池、靜態變量也移到堆區進行存儲堆區被細化可以分爲年輕代、老年代,而年輕代又可分爲Eden區、From Survivor、To Survivor三個區域,結構圖如下所示:比例是8:1:1。

堆的大小可以通過-Xms(初始堆大小)和-Xmx(最大堆大小)來控制。如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常 

 

技術分享

爲什麼會有年輕代?

我們先來屢屢,爲什麼需要把堆分代?不分代不能完成他所做的事情麼?其實不分代完全可以,分代的唯一理由就是優化GC性能。你先想想,如果沒有分代,那我們所有的對象都在一塊,GC的時候我們要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而我們的很多對象都是朝生夕死的,如果分代的話,我們把新創建的對象放到某一地方,當GC的時候先把這塊存“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。

 

HotSpot JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例爲8:1,爲啥默認會是這個比例,接下來我們會聊到。一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。

因爲年輕代中的對象基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另外一塊上面。複製算法不會產生內存碎片。

在GC開始的時候,對象只會存在於Eden區和名爲“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有對象移動到年老代中。

young_gc

簡單來說:

  • 新生代。新建的對象都是用新生代分配內存,Eden空間不足的時候,會把存活的對象轉移到Survivor中,新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例

  • 舊生代。用於存放新生代中經過多次垃圾回收仍然存活的對象

  • 持久帶(Permanent Space)實現方法區,主要存放所有已加載的類信息,方法信息,常量池等等。可通過-XX:PermSize和-XX:MaxPermSize來指定持久帶初始化值和最大值。Permanent Space並不等同於方法區,只不過是Hotspot JVM用Permanent Space來實現方法區而已,有些虛擬機沒

2、方法區 
方法區主要是存儲類的元數據的,如虛擬機加載的類信息、編譯後的代碼等。JDK8之前方法區的實現是被稱爲一種“永久代”的區域,這部分區域使用JVM內存,但是JDK8的時候便移除了“永久代(Per Gen)”,轉而使用“元空間(MetaSpace)”的實現,而且很大的不同就是元空間不在共用JVM內存,而是使用的系統內存。 在JDK8中,方法區的實現已經由永久代轉變成了元空間

一個對象的一輩子過程:

我是一個普通的java對象,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,我就開始漂了,有時候在Survivor的“From”區,有時候在Survivor的“To”區,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。於是我就去了年老代那邊,年老代裏,人很多,並且年齡都挺大的,我在這裏也認識了很多人。在年老代裏,我生活了20年(每次GC加一歲),然後被回收。

 

3、虛擬機棧 
我們通常所說的“方法入棧”、“棧區”其實指代的就是虛擬機棧。但是實際上這個並不準確,我們所說的“棧區”等稱呼確切的說指代的是虛擬機棧中局部變量表的部分。 
Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。 
局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同於對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。 
其中64位長度的long和double類型的數據會佔用2個局部變量空間(Slot),其餘的數據類型只佔用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。 
在Java虛擬機規範中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規範中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時會拋出OutOfMemoryError異常。 
4、本地方法棧 
從英文中我們可以很容易的看出,其實這部分區域是專門爲Native方法來實現的!由於java需要與一些底層系統如操作系統或某些硬件交換信息時的情況。本地方法正是這樣一種交流機制:它爲我們提供了一個非常簡潔的接口,而且我們無需去了解java應用之外的繁瑣的細節 ,一個Native Method就是一個java調用非java代碼的接口。方法對應的實現不是在當前文件,而是在用其他語言(如C和C++)實現的文件中。Java語言本身不能對操作系統底層進行訪問和操作,但是可以通過JNI接口調用其他語言來實現對底層的訪問。 
5、程序計數器 
程序計數器(Program Counter Register)是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。 
由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。 。多線程時,當線程數超過CPU數量或CPU內核數量,線程之間就要根據時間片輪詢搶奪CPU時間資源。因此每個線程有要有一個獨立的程序計數器,記錄下一條要運行的指令。線程私有的內存區域。如果線程執行的是JAVA方法,計數器記錄正在執行的虛擬機字節碼指令的地址,如果執行的是native方法,則計數器爲空(Undefined)。

 

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