java虛擬機內存分區

內存模型以及分區

JVM內存模型如下圖所示:
JVM內存模型

JVM內存模型

此處我們集中注意中間綠色的部分,該部分爲JVM的運行時內存,該部分包含了:

線程私有的(灰色):
  1. 程序計數器:記錄執行到第幾條指令
  2. 虛擬機方法棧:執行Java方法所用,每執行一個方法便加入一個棧幀,裏面含有局部變量表、操作棧、動態鏈接和方法出口等
  3. 本地方法棧:與虛擬機方法棧相似,用於執行native方法
線程共享的(藍色):
  1. 堆:對象實例存放地,,分爲年輕代和老年代。年輕代又細分爲伊甸園區和兩個相對的Survival區
  2. 方法區:也叫永久代,存放了類的信息、常量、類靜態變量等元素,含有一個運行時常量池

堆裏面的分區:Eden,survival from to,老年代,各自的特點

Eden區

Eden區位於Java堆的年輕代,是新對象分配內存的地方,由於堆是所有線程共享的,因此在堆上分配內存需要加鎖。而Sun JDK爲提升效率,會爲每個新建的線程在Eden上分配一塊獨立的空間由該線程獨享,這塊空間稱爲TLAB(Thread Local Allocation Buffer)。在TLAB上分配內存不需要加鎖,因此JVM在給線程中的對象分配內存時會盡量在TLAB上分配。如果對象過大或TLAB用完,則仍然在堆上進行分配。如果Eden區內存也用完了,則會進行一次Minor GC(young GC)。

Survival from to

Survival區與Eden區相同都在Java堆的年輕代。Survival區有兩塊,一塊稱爲from區,另一塊爲to區,這兩個區是相對的,在發生一次Minor GC後,from區就會和to區互換。在發生Minor GC時,Eden區和Survival from區會把一些仍然存活的對象複製進Survival to區,並清除內存。Survival to區會把一些存活得足夠舊的對象移至年老代。

年老代

年老代裏存放的都是存活時間較久的,大小較大的對象,因此年老代使用標記整理算法。當年老代容量滿的時候,會觸發一次Major GC(full GC),回收年老代和年輕代中不再被使用的對象資源。
各個分區的流動

也就是說,新生代裏面存放的Eden區是用來放最新對象的堆內存爲每個線程提供了一個TLAB(Thread Local Allocation Buffer)的內存,這塊區域存放的對象是單獨爲這個線程服務的。如果對象特別大,或者TLAB用完了就對當前Eden區進行一次內存回收(young GC),清除沒有活動的對象。

Survival區做爲第二層級,survival區中有兩個區,from區和to區,當 JVM 無法爲一個新的對象分配空間時會觸發 Minor GC,比如當 Eden 區滿了。所以分配率越高,越頻繁執行 Minor GC。執行完畢之後會將Eden,和from區還在活動的對象放進to區。將to區不在活動的對象放進from區。將to區一些存活得足夠舊的對象移至年老代。

年老帶在容量滿的時候會促發Major GC(FULL GC),回收年老代和年輕代中不再被使用的對象

對象創建方法,對象的內存分配,對象的訪問定位

對象的創建

Java對象的創建大致上有以下幾個步驟:
1. 類加載檢查:檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類的加載過程
2. 爲對象分配內存:對象所需內存的大小在類加載完成後便完全確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。由於堆被線程共享,因此此過程需要進行同步處理(分配在TLAB上不需要同步)
3. 內存空間初始化:虛擬機將分配到的內存空間都初始化爲零值(不包括對象頭),內存空間初始化保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
4. 對象設置:JVM對對象頭進行必要的設置,保存一些對象的信息(指明是哪個類的實例,哈希碼,GC年齡等)
5. init:執行完上面的4個步驟後,對JVM來說對象已經創建完畢了,但對於Java程序來說,我們還需要對對象進行一些必要的初始化。

對象的內存分配

Java對象的內存分配有兩種情況,由Java堆是否規整來決定(Java堆是否規整由所採用的垃圾收集器是否帶有壓縮整理功能決定):
1. 指針碰撞(Bump the pointer):如果Java堆中的內存是規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,分配內存也就是把指針向空閒空間那邊移動一段與內存大小相等的距離
2. 空閒列表(Free List):如果Java堆中的內存不是規整的,已使用的內存和空閒的內存相互交錯,就沒有辦法簡單的進行指針碰撞了。虛擬機必須維護一張列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄

對象的訪問定位

對象的訪問形式取決於虛擬機的實現,目前主流的訪問方式有使用句柄和直接指針兩種:
1. 使用句柄:
如果使用句柄訪問,Java堆中將會劃分出一塊內存來作爲句柄池,引用中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息:
這裏寫圖片描述

優勢:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而引用本身不需要修改。


  1. 直接指針:
    如果使用直接指針訪問對象,那麼對象的實例數據中就包含一個指向對象類型數據的指針,引用中存的直接就是對象的地址:
    這裏寫圖片描述

優勢:速度更快,節省了一次指針定位的時間開銷,積少成多的效應非常可觀。

GC的兩種判定方法:引用計數與引用鏈

基於引用計數與基於引用鏈這兩大類別的自動內存管理方式最大的不同之處在於:前者只需要局部信息,而後者需要全局信息

引用計數

引用計數顧名思義,就是記錄下一個對象被引用指向的次數。引用計數方式最基本的形態就是讓每個被管理的對象與一個引用計數器關聯在一起,該計數器記錄着該對象當前被引用的次數,每當創建一個新的引用指向該對象時其計數器就加1,每當指向該對象的引用失效時計數器就減1。當該計數器的值降到0就認爲對象死亡。每個計數器只記錄了其對應對象的局部信息——被引用的次數,而沒有(也不需要)一份全局的對象圖的生死信息。由於只維護局部信息,所以不需要掃描全局對象圖就可以識別並釋放死對象;但也因爲缺乏全局對象圖信息,所以無法處理循環引用的狀況。

引用鏈

引用鏈需要內存的全局信息,當使用引用鏈進行GC時,從對象圖的“根”(GC Root,必然是活的引用,包括棧中的引用,類靜態屬性的引用,常量的引用,JNI的引用等)出發掃描出去,基於引用的可到達性算法來判斷對象的生死。這使得對象的生死狀態能批量的被識別出來,然後批量釋放死對象。引用鏈不需要顯式維護對象的引用計數,只在GC使用可達性算法遍歷全局信息的時候判斷對象是否被引用,是否存活。

GC的三種收集方法:標記清除、標記整理、複製算法的原理與特點,分別用在什麼地方

標記清除

標記清除算法分兩步執行:
暫停用戶線程,通過GC Root使用可達性算法標記存活對象
清除未被標記的垃圾對象
標記清除算法缺點如下:
效率較低,需要暫停用戶線程
清除垃圾對象後內存空間不連續,存在較多內存碎片
標記算法如今使用的較少了

複製算法

複製算法也分兩步執行,在複製算法中一般會有至少兩片的內存空間(一片是活動空間,裏面含有各種對象,另一片是空閒空間,裏面是空的):
暫停用戶線程,標記活動空間的存活對象
把活動空間的存活對象複製到空閒空間去,清除活動空間
複製算法相比標記清除算法,優勢在於其垃圾回收後的內存是連續的。
但是複製算法的缺點也很明顯:
需要浪費一定的內存作爲空閒空間
如果對象的存活率很高,則需要複製大量存活對象,導致效率低下
複製算法一般用於年輕代的Minor GC,主要是因爲年輕代的大部分對象存活率都較低

標記整理

標記整理算法是標記清除算法的改進,分爲標記、整理兩步:
暫停用戶線程,標記所有存活對象
移動所有存活對象,按內存地址次序一次排列,回收末端對象以後的內存空間
標記整理算法與標記清除算法相比,整理出的內存是連續的;而與複製算法相比,不需要多片內存空間。
然而標記整理算法的第二步整理過程較爲麻煩,需要整理存活對象的引用地址,理論上來說效率要低於複製算法。
因此標記整理算法一般引用於老年代的Major GC

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