1. 程式人生 > >jvm簡介及其記憶體分佈介紹(入門級)

jvm簡介及其記憶體分佈介紹(入門級)

一.jvm執行機制

jvm啟動流程:
這裡寫圖片描述

  1. java虛擬機器啟動的命令是通過java +xxx(類名,這個類中要有main方法)或者javaw啟動的。
  2. 執行命令後,系統第一步做的就是裝載配置,會在當前路徑中尋找jvm的config配置檔案。
  3. 找到jvm的config配置檔案之後會去定位jvm.dll這個檔案。這個檔案就是java虛擬機器的主要實現。
  4. 當找到匹配當前版本的jvm.dll檔案後,就會使用這個dll去初始化jvm虛擬機器。獲得相關的介面。之後找到main方法開始執行。

java基礎結構:
這裡寫圖片描述
pc暫存器
每個執行緒擁有一個pc暫存器。
jvm會為每一個執行緒分配一個pc暫存器,這個pc暫存器總是會指向下一個指令的地址。這樣程式在執行過程中pc暫存器總是會知道下一步會做什麼。在執行本地方法的時候,pc暫存器的值總是未定義的。
方法區


方法區是用來儲存類的原資訊。用來描述類的資訊,包括型別常量池,欄位方法資訊,方法位元組碼。在JDK6的時候字串常量是放在方法區中,但是JDK7的時候就已經移到了堆中。所以從這方面來說方法區,堆中到底儲存的是什麼資訊和jdk的版本有很大的關係。從一般意義上來說我們的方法區就是儲存一些類的原資訊。方法區通常和永久區(perm)關聯在一起,儲存一些相對穩定的資料,
java堆
1.java堆應該是和程式開發中最為密切的一個記憶體區間,我們在程式開發中通過new出來的物件基本上都是儲存在java堆中。
2.堆是全域性共享的,所有執行緒都共享java堆,也就是你建立了一個物件之後,所有的執行緒都是能夠訪問的。
3.從GC的角度看,java堆的結構和GC的演算法是有關係的。
java棧

java棧和堆相比是執行緒私有的,棧是由一系列幀組成的,所以java棧也叫作幀棧。幀中儲存的內容是一個方法的區域性變數,運算元棧,常量池指標。每一次方法呼叫都會建立一個新的幀,並壓棧。

二.JVM的記憶體區域劃分

Java程式具體執行的過程:
這裡寫圖片描述
如上圖所示,首先Java原始碼檔案(.java字尾)會被Java編譯器編譯為位元組碼檔案(.class字尾),然後由JVM中的類載入器載入各個類的位元組碼檔案,載入完畢之後,交由JVM執行引擎執行。在整個程式執行過程中,JVM會用一段空間來儲存程式執行期間需要用到的資料和相關資訊,這段空間一般被稱作為Runtime Data Area(執行時資料區),也就是我們常說的JVM記憶體。因此,在Java中我們常常說到的記憶體管理就是針對這段空間進行管理(如何分配和回收記憶體空間)。
1.執行時資料區包括哪幾部分
根據《Java虛擬機器規範》的規定,執行時資料區通常包括這幾個部分:程式計數器(Program Counter Register)、Java棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(Method Area)、堆(Heap)。
這裡寫圖片描述


如上圖所示,JVM中的執行時資料區應該包括這些部分。在JVM規範中雖然規定了程式在執行期間執行時資料區應該包括這幾部分,但是至於具體如何實現並沒有做出規定,不同的虛擬機器廠商可以有不同的實現方式。

三.Java記憶體分配機制
這裡所說的記憶體分配,主要指的是在堆上的分配,一般的,物件的記憶體分配都是在堆上進行,但現代技術也支援將物件拆成標量型別(標量型別即原子型別,表示單個值,可以是基本型別或String等),然後在棧上分配,在棧上分配的很少見,我們這裡不考慮。
  Java記憶體分配和回收的機制概括的說,就是:分代分配,分代回收。物件將根據存活的時間被分為:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區)。如下圖(來源於《成為JavaGC專家part I》,http://www.importnew.com/1993.html):
  這裡寫圖片描述
年輕代(Young Generation):物件被建立時,記憶體的分配首先發生在年輕代(大物件可以直接被建立在年老代),大部分的物件在建立後很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制清理掉(IBM的研究表明,98%的物件都是很快消亡的),這個GC機制被稱為Minor GC或叫Young GC。注意,Minor GC並不代表年輕代記憶體不足,它事實上只表示在Eden區上的GC。
  年輕代上的記憶體分配是這樣的,年輕代可以分為3個區域:Eden區(伊甸園,亞當和夏娃偷吃禁果生娃娃的地方,用來表示記憶體首次分配的區域,再貼切不過)和兩個存活區(Survivor 0 、Survivor 1)。記憶體分配過程為(來源於《成為JavaGC專家part I》,http://www.importnew.com/1993.html):
這裡寫圖片描述
1. 絕大多數剛建立的物件會被分配在Eden區,其中的大多數物件很快就會消亡。Eden區是連續的記憶體空間,因此在其上分配記憶體極快;
2. 最初一次,當Eden區滿的時候,執行Minor GC,將消亡的物件清理掉,並將剩餘的物件複製到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);
3. 下次Eden區滿了,再執行一次Minor GC,將消亡的物件清理掉,將存活的物件複製到Survivor1中,然後清空Eden區;
4. 將Survivor0中消亡的物件清理掉,將其中可以晉級的物件晉級到Old區,將存活的物件也複製到Survivor1區,然後清空Survivor0區;
5. 當兩個存活區切換了幾次(HotSpot虛擬機器預設15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代,但這只是個最大值,並不代表一定是這個值)之後,仍然存活的物件(其實只有一小部分,比如,我們自己定義的物件),將被複制到老年代。
  從上面的過程可以看出,Eden區是連續的空間,且Survivor總有一個為空。經過一次GC和複製,一個Survivor中儲存著當前還活著的物件,而Eden區和另一個Survivor區的內容都不再需要了,可以直接清空,到下一次GC時,兩個Survivor的角色再互換。因此,這種方式分配記憶體和清理記憶體的效率都極高,這種垃圾回收的方式就是著名的“停止-複製(Stop-and-copy)”清理法(將Eden區和一個Survivor中仍然存活的物件拷貝到另一個Survivor中),這不代表著停止複製清理法很高效,其實,它也只在這種情況下高效,如果在老年代採用停止複製,則挺悲劇的。
  在Eden區,HotSpot虛擬機器使用了兩種技術來加快記憶體分配。分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),這兩種技術的做法分別是:由於Eden區是連續的,因此bump-the-pointer技術的核心就是跟蹤最後建立的一個物件,在物件建立時,只需要檢查最後一個物件後面是否有足夠的記憶體即可,從而大大加快記憶體分配速度;而對於TLAB技術是對於多執行緒而言的,將Eden區分為若干段,每個執行緒使用獨立的一段,避免相互影響。TLAB結合bump-the-pointer技術,將保證每個執行緒都使用Eden區的一段,並快速的分配記憶體。
  年老代(Old Generation):物件如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC後存活了下來),則會被複制到年老代,年老代的空間一般比年輕代大,能存放更多的物件,在年老代上發生的GC次數也比年輕代少。當年老代記憶體不足時,將執行Major GC,也叫 Full GC。  
  可以使用-XX:+UseAdaptiveSizePolicy開關來控制是否採用動態控制策略,如果動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。
  如果物件比較大(比如長字串或大陣列),Young空間不足,則大物件會直接分配到老年代上(大物件可能觸發提前GC,應少用,更應避免使用短命的大物件)。用-XX:PretenureSizeThreshold來控制直接升入老年代的物件大小,大於這個值的物件會直接分配在老年代上。
  可能存在年老代物件引用新生代物件的情況,如果需要執行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——”card table“,所有老年代物件引用新生代物件的記錄都記錄在這裡。Young GC時,只要查這裡即可,不用再去查全部老年代,因此效能大大提高。
Java GC機制
GC機制的基本演算法是:分代收集,這個不用贅述。下面闡述每個分代的收集方法。
  
  年輕代:
  事實上,在上一節,已經介紹了新生代的主要垃圾回收方法,在新生代中,使用“停止-複製”演算法進行清理,將新生代記憶體分為2部分,1部分 Eden區較大,1部分Survivor比較小,並被劃分為兩個等量的部分。每次進行清理時,將Eden區和一個Survivor中仍然存活的物件拷貝到 另一個Survivor中,然後清理掉Eden和剛才的Survivor。
  這裡也可以發現,停止複製演算法中,用來複制的兩部分並不總是相等的(傳統的停止複製演算法兩部分記憶體相等,但新生代中使用1個大的Eden區和2個小的Survivor區來避免這個問題)
  由於絕大部分的物件都是短命的,甚至存活不到Survivor中,所以,Eden區與Survivor的比例較大,HotSpot預設是 8:1,即分別佔新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下來的記憶體超過了10%,則需要將一部分物件分配到 老年代。用-XX:SurvivorRatio引數來配置Eden區域Survivor區的容量比值,預設是8,代表Eden:Survivor1:Survivor2=8:1:1.
  老年代:
  老年代儲存的物件比年輕代多得多,而且不乏大物件,對老年代進行記憶體清理時,如果使用停止-複製演算法,則相當低效。一般,老年代用的演算法是標記-整理演算法,即:標記出仍然存活的物件(存在引用的),將所有存活的物件向一端移動,以保證記憶體的連續。
在發生Minor GC時,虛擬機器會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,如果大於,則直接觸發一次Full GC,否則,就檢視是否設定了-XX:+HandlePromotionFailure(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍記憶體分配失敗;如果不允許,則仍然進行Full GC(這代表著如果設定-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多記憶體,所以,最好不要這樣做)。
  方法區(永久代):
  永久代的回收有兩種:常量池中的常量,無用的類資訊,常量的回收很簡單,沒有引用了就可以被回收。對於無用的類進行回收,必須保證3點:
1. 類的所有例項都已經被回收
2. 載入類的ClassLoader已經被回收
3. 類物件的Class物件沒有被引用(即沒有通過反射引用該類的地方)
永久代的回收並不是必須的,可以通過引數來設定是否對類進行回收。HotSpot提供-Xnoclassgc進行控制
使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以檢視類載入和解除安裝資訊
-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;
-XX:+TraceClassUnLoading需要fastdebug版HotSpot支援