我看Java虛擬機(2)---Java虛擬機內存區域詳解

虛擬機內存區域的組成

直接上圖:
這裏寫圖片描述

  • 程序計數器:對於Java方法,用來選取下一條要執行的字節碼;對於本地方法,值爲空。線程獨有
  • 虛擬機棧:執行Java方法,每一層都是一個棧幀,棧幀包括局部變量表、操作數棧、動態鏈接和方法出口等信息。線程獨有
  • 本地方法棧:執行Native方法,sun HotSpot將其與虛擬機棧合二爲一。
  • :存放對象實例。堆分爲新生代和老生代,新生代分爲Eden區和兩個Survivor區,默認Eden和Survivor(一個Survivor)之比爲8:1。所有線程共享
  • 方法區:HotShot將其實現爲永生代。虛擬機讀入(javac編譯器生成)class文件,將會存儲信息到該部分,則該部分會存儲虛擬機加載的類信息,常量,靜態變量即時編譯器編譯後的代碼。該部分還有一個重要的組成部分——常量池。所有線程共享
  • 直接內存:嚴格來說這部分並不屬於Java虛擬機的內存區域,不過Java 在JDK1.4中新加入了NIO類,引入了基於通道和緩衝區的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後在Java堆中生成一個該內存地址的引用,使用該引用來操作堆外內存中的對象。

內存分配和垃圾收集

  • 程序計數器不存在內存分配的問題。
  • 虛擬機棧和本地方發棧是在運行時,給將要運行的方法分配內存。
  • 方法區在類加載的時候分配內存。
    以下都是主要研究堆內存的分配和垃圾回收

內存分配

堆中區域圖:

這裏寫圖片描述
主要有以下幾點:
對象優先在Eden區分配:當Eden區內存不足時,將發起一次Minor GC(Garbage Collection),需要將Eden區和Survivor A(筆者自行編的號,另一個則編號Survivor B)區中,仍舊存活的數據全部複製到Survivor B區中;當下一次GC時,則Survivor A和Survivor B互換位置,即Survivor B+Eden —–複製到—–》Survivor A,如此循環,循環,循環。。。直到Survivor 再也不能容納Eden+Survivor的時候,老生代就不能再閒着了,就會選擇一些數據放置到老生代了。那麼問題來了,要選擇哪些數據去老生代呢?即選擇的標準是什麼,接着往下看。
老生代:能進入老生代的數據分兩種:“先天條件好的”大對象和“後天足夠努力的“頑強者。先天條件好的大對象享受特權,可直接分配內存到老生代,主要是考慮到當發生Minor GC(當作城管)時,若管理的區域全是有背景的大對象,清理起來特別不方便,影響裝X;那麼問題又來了:多大才是大?虛擬機提供了-XX:PretenureSizeThreshold參數(只對Serial和ParNew兩個收集器有效,垃圾收集會講到)來設置。
小對象就沒了這種顧慮,讓你去Survivor A絕不會去Survivor B,正所謂大浪淘沙,爲了能挑選出頑強的對象,有兩種方式來判定:

  1. 計數器:給每一個新生代的對象配一個計數器,當發生一次Minor GC,對象移動一次,計數器+1,直到達到設定的值(默認15,可通過–XX:MaxTenuringThreshhold設置),就可以進入老生代。
  2. 動態判定:當新生代相同年齡的對象的大小之和大於Survivor(一個)的一半,則將該年齡的對象和比他們老的對象全部進入老生代。

當新生代對象需要進入老生代時,老生代也不是無限大的,所以保不齊需要複製進老生代的對象,其大小會超出老生代的最大容量,這時候,就會進行分配擔保。簡單說,虛擬機會根據以往,每次晉升到老生代所需分配內存的平均值,比較老生代剩餘空間,如果大於剩餘(即剩餘空間不足),則進行一次Full GC(清理老生代);如果小於剩餘,則查看HandlePromotionFailure設置是否允許擔保失敗,如果允許,則進行Minor GC,否則Full GC。

垃圾收集

三個知識點:

  1. 判斷對象死亡
  2. 垃圾收集算法
  3. 垃圾收集器
    判斷對象死亡
    兩種算法:
    • 計數器:當對象有一條引用時,其引用計數器加一,當計數器爲0時,可判斷其死亡。缺陷:當堆中對象互相引用時,即使外部沒有了指向該對象的引用,他們計數器也不爲0,不能被回收,如圖:
      這裏寫圖片描述
    • 根搜索:每個節點都是一個對象,有一個根節點,當有節點到根節點不可達時,即可判斷該對象死亡。Java和C#都使用該算法。如圖4,5,6都可回收:
      這裏寫圖片描述
      垃圾收集算法
      標記-清理算法:首先,標記需要清除的對象,然後清除被標記的對象。缺點是,碎片化太嚴重。
      複製算法:新生代使用的算法
      標記-整理:比標記-清理,多出整理這一步。老生代使用的算法
      分代收集算法:複製算法和標記-整理算法的簡單相加。
      垃圾收集器
      盜來的圖:
      這裏寫圖片描述
      新生代(複製算法):Serial(單線程),ParNew(多線程),Parallel (注重吞吐量)
      老生代(標記-整理):Serial Old,CMS,Parallel Old

對象訪問

事實上,堆中對象除了存儲對象本身外,還要存儲其類型信息,而類型信息存儲於方法區,那麼該對象就需要存儲一個指向方法區的指針。
當使用一個引用reference訪問對象時,主流的訪問方式有兩種:句柄訪問方式直接指針訪問方式
句柄訪問方式:兩個指針,一個指向真實的對象,一個指向類型信息(堆中兩個指針,一個對象);
如圖:這裏寫圖片描述
直接指針訪問方式:真實的對象和指向類型信息的指針(堆中一個指針,一個對象)。
如圖:這裏寫圖片描述

聰明的你一定會疑問,爲什麼要有句柄訪問方式,多此一舉,直接指針就好了,幹嘛要多出一個指針來,我選擇第二種。事實上,sun Hotspot虛擬機也選用的第二種。可存在必合理,第一種到底是基於什麼考慮?
共識:垃圾收集時,堆中的對象移動是非常普遍的行爲(前面講到了)。如果採用句柄式的話,就無需改變reference的值,只需要改變一個指向對象本身的指針即可。直接訪問方式的話,當對象移動時,那就需要改變reference的值了。
直接訪問方式的優勢也就是訪問速度更快,節省一次指針定位的時間,由於對象訪問在程序中非常頻繁,HotSpot虛擬機也是基於這種考慮吧!
疑惑:當對象移動時,我們在使用HotSpot虛擬機下寫程序時,並未手動改變過reference的值,reference又是怎麼定位到已經移動過的對象的?這次筆者真是不知道了
又是萬能的知乎:當發生一次GC時,對象移動之後會自動刷新一次引用reference。似乎想的通,希望大神們不吝賜教。


思考總結:
“對象存放於堆,基本類型存放於棧”,這句話準確嗎?如果準確,怎麼對應於上面的區域?否則,哪裏不準確?
答案是不準確。
不知道大家有沒有發現,上面的解釋敘述了類變量存放於方法區;實例變量中,全局對象存放於堆中,局部變量基本類型和對象引用存放於虛擬機棧中,對象實例存放於堆。唯獨沒有說明全局基本類型存放的位置,網上大部分說的對象存放於堆,基本類型存放於棧這種說法,可觀衆朋友們,你們是學習過Java虛擬機的高級程序員,能這麼膚淺嗎?就算是棧,就那兩棧,誰能收留基本類型?沒有一個。
萬能的知乎已經告訴了我們答案,是堆!話不多說,
進入副本看答案,我就不搬運了。
下一節,講解類文件結構,想想以後可以自己可以將calss文件反編譯爲Java代碼,是不是還有點小激動,騷年,我看好你。

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