jvm原理,記憶體模型及GC機制
1. jvm記憶體模型
程式計數器、本地方法棧、方法區、Java棧、Java堆及其他隱含暫存器。
1.1 程式計數器:
程式計數器是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
由於Java 虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有
如果執行緒正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空(Undefined)。
1.2 本地方法棧:
本地方法棧(Native MethodStacks)與虛擬機器棧所發揮的作用是非常相似的,其區別不過是虛擬機器棧為虛擬機器執行Java 方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native 方法服務。虛擬機器規範中對本地方法棧中的方法使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(譬如Sun HotSpot 虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一
與虛擬機器棧一樣,本地方法棧區域也會丟擲StackOverflowError和OutOfMemoryError異常。
1.3 方法區:
方法區在一個jvm例項的內部,型別資訊被儲存在一個稱為方法區的記憶體邏輯區中。型別資訊是由類載入器在類載入時從類檔案中提取出來的。類(靜態)變數也儲存在方法區中。
簡單說方法區用來儲存型別的元資料資訊,一個.class檔案是類被java虛擬機器使用之前的表現形式,一旦這個類要被使用,java虛擬機器就會對其進行裝載、連線(驗證、準備、解析)和初始化。而裝載(後的結果就是由.class檔案轉變為方法區中的一段特定的資料結構。這個資料結構會儲存如下資訊:
1.3.1 型別資訊
這個型別的全限定名
這個型別的直接超類的全限定名
這個型別是類型別還是介面型別
這個型別的訪問修飾符
任何直接超介面的全限定名的有序列表
1.3.2 欄位資訊
欄位名
欄位型別
欄位的修飾符
1.3.3 方法資訊
方法名
方法返回型別
方法引數的數量和型別(按照順序)
方法的修飾符
1.3.4 其他資訊
除了常量以外的所有類(靜態)變數
一個指向ClassLoader的指標
一個指向Class物件的指標
常量池(常量資料以及對其他型別的符號引用)
方法區主要有以下幾個特點:
1、方法區是執行緒安全的。由於所有的執行緒都共享方法區,所以,方法區裡的資料訪問必須被設計成執行緒安全的。例如,假如同時有兩個執行緒都企圖訪問方法區中的同一個類,而這個類還沒有被裝入JVM,那麼只允許一個執行緒去裝載它,而其它執行緒必須等待
2、方法區的大小不必是固定的,JVM可根據應用需要動態調整。同時,方法區也不一定是連續的,方法區可以在一個堆(甚至是JVM自己的堆)中自由分配。
3、方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將解除安裝這個類,進行垃圾收集
可以通過-XX:PermSize 和 -XX:MaxPermSize 引數限制方法區的大小。
1.4 虛擬機器棧(java棧)
執行緒私有,它的生命週期與執行緒相同。虛擬機器棧描述的是Java 方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變量表、操作棧、動態連結、方法出口等資訊。
動畫是由一幀一幀圖片連續切換結果的結果而產生的,其實虛擬機器的執行和動畫也類似,每個在虛擬機器中執行的程式也是由許多的幀的切換產生的結果,只是這些幀裡面存放的是方法的區域性變數,運算元棧,動態連結,方法返回地址和一些額外的附加資訊組成。每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。
對於執行引擎來說,活動執行緒中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法。執行引擎所執行的所有位元組碼指令都只針對當前棧幀進行操作。
1.5 堆
堆是Java 虛擬機器所管理的記憶體中最大的一塊。Java 堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。但是隨著JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的物件都分配在堆上也漸漸變得不是那麼“絕對”了。
堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”。
1.5.1 堆記憶體與棧記憶體需要說明:
基礎資料型別直接在棧空間分配,方法的形式引數,直接在棧空間分配,當方法呼叫完成後從棧空間回收。引用資料型別,需要用new來建立,既在棧空間分配一個地址空間,又在堆空間分配物件的類變數 。方法的引用引數,在棧空間分配一個地址空間,並指向堆空間的物件區,當方法呼叫完成後從棧空間回收。區域性變數new出來時,在棧空間和堆空間中分配空間,當局部變數生命週期結束後,棧空間立刻被回收,堆空間區域等待GC回收。方法呼叫時傳入的literal引數,先在棧空間分配,在方法呼叫完成後從棧空間收回。字串常量、static在DATA區域分配,this在堆空間分配。陣列既在棧空間分配陣列名稱,又在堆空間分配陣列實際的大小。
總結
名稱 |
特徵 |
作用 |
配置引數 |
異常 |
程式計數器 |
佔用記憶體小,執行緒私有, 生命週期與執行緒相同 |
大致為位元組碼行號指示器 |
無 |
無 |
虛擬機器棧 |
執行緒私有,生命週期與執行緒相同,使用連續的記憶體空間 |
Java 方法執行的記憶體模型,儲存區域性變量表、操作棧、動態連結、方法出口等資訊 |
-Xss |
StackOverflowError OutOfMemoryError |
java堆 |
執行緒共享,生命週期與虛擬機器相同,可以不使用連續的記憶體地址 |
儲存物件例項,所有物件例項(包括陣列)都要在堆上分配 |
-Xms -Xsx -Xmn |
OutOfMemoryError |
方法區 |
執行緒共享,生命週期與虛擬機器相同,可以不使用連續的記憶體地址 |
儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料 |
-XX:PermSize: 16M -XX:MaxPermSize 64M |
OutOfMemoryError |
執行時常量池 |
方法區的一部分,具有動態性 |
存放字面量及符號引用 |
2.GC機制
垃圾收集器一般必須完成兩件事:檢測出垃圾;回收垃圾。怎麼檢測出垃圾?一般有以下幾種方法:
2.1 引用計數法:
給一個物件新增引用計數器,每當有個地方引用它,計數器就加1;引用失效就減1。
好了,問題來了,如果我有兩個物件A和B,互相引用,除此之外,沒有其他任何物件引用它們,實際上這兩個物件已經無法訪問,即是我們說的垃圾物件。但是互相引用,計數不為0,導致無法回收,所以還有另一種方法:
2.2 可達性分析演算法:
以根集物件為起始點進行搜尋,如果有物件不可達的話,即是垃圾物件。這裡的根集一般包括java棧中引用的物件、方法區常量池中引用的物件、本地方法中引用的物件等。
總之,JVM在做垃圾回收的時候,會檢查堆中的所有物件是否會被這些根集物件引用,不能夠被引用的物件就會被垃圾收集器回收。
2.3 一般回收演算法也有如下幾種:
2.3.1 按照基本回收策略分
(1)標記-清除(Mark-sweep)
演算法和名字一樣,分為兩個階段:標記和清除。標記所有需要回收的物件,然後統一回收。這是最基礎的演算法,後續的收集演算法都是基於這個演算法擴充套件的。
不足:效率低;標記清除之後會產生大量碎片。效果圖如下:
(2)複製(Copying)
此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中。此演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不會出現“碎片”問題。當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間。效果圖如下:
(3)標記-整理(Mark-Compact)
此演算法結合了“標記-清除”和“複製”兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,把清除未標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。效果圖如下:
2.3.2 按分割槽對待的方式分
(1)增量收集(Incremental Collecting):實時垃圾回收演算法,即:在應用進行的同時進行垃圾回收。不知道什麼原因JDK5.0中的收集器沒有使用這種演算法的。
(2)分代收集(Generational Collecting):基於對物件生命週期分析後得出的垃圾回收演算法。把物件分為年青代、年老代、持久代,對不同生命週期的物件使用不同的演算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此演算法的。
2.3.3 按系統執行緒分
(1)序列收集:序列收集使用單執行緒處理所有垃圾回收工作,因為無需多執行緒互動,實現容易,而且效率比較高。但是,其侷限性也比較明顯,即無法使用多處理器的優勢,所以此收集適合單處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。
(2)並行收集:並行收集使用多執行緒處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能體現出並行收集器的優勢。
(3)併發收集:相對於序列收集和並行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個執行環境,而只有垃圾回收程式在執行,因此,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因為堆越大而越長。
(注:參考自 http://pengjiaheng.iteye.com/blog/520228)
2.4 虛擬機器的GC過程
2.4.1 為什麼要分代回收
在一開始的時候,JVM的GC就是採用標記-清除-壓縮方式進行的,這麼做並不是很高效,因為當物件分配的越來越多時,物件列表也越來也大,掃描和移動越來越耗時,造成了記憶體回收越來越慢。然而,經過根據對java應用的分析,發現大部分物件的存活時間都非常短,只有少部分資料存活週期是比較長的,請看下面對java物件記憶體存活時間的統計:
從圖表中可以看出,大部分物件存活時間是非常短的,隨著時間的推移,被分配的物件越來越少。
2.4.2 虛擬機器的GC過程
經過上面介紹,我們已經知道了JVM為何要分代回收,下面我們就詳細看一下整個回收過程。
-
在初始階段,新建立的物件被分配到Eden區,survivor的兩塊空間都為空。
-
當Eden區滿了的時候,minor garbage 被觸發
-
經過掃描與標記,存活的物件被複制到S0,不存活的物件被回收
-
在下一次的Minor GC中,Eden區的情況和上面一致,沒有引用的物件被回收,存活的物件被複制到survivor區。然而在survivor區,S0的所有的資料都被複制到S1,需要注意的是,在上次minor GC過程中移動到S0中的兩個物件在複製到S1後其年齡要加1。此時Eden區S0區被清空,所有存活的資料都複製到了S1區,並且S1區存在著年齡不一樣的物件,過程如下圖所示:
-
再下一次MinorGC則重複這個過程,這一次survivor的兩個區對換,存活的物件被複制到S0,存活的物件年齡加1,Eden區和另一個survivor區被清空。
-
下面演示一下Promotion過程,再經過幾次Minor GC之後,當存活物件的年齡達到一個閾值之後(可通過引數配置,預設是8),就會被從年輕代Promotion到老年代。
-
隨著MinorGC一次又一次的進行,不斷會有新的物件被promote到老年代。
-
上面基本上覆蓋了整個年輕代所有的回收過程。最終,MajorGC將會在老年代發生,老年代的空間將會被清除和壓縮。
從上面的過程可以看出,Eden區是連續的空間,且Survivor總有一個為空。經過一次GC和複製,一個Survivor中儲存著當前還活著的物件,而Eden區和另一個Survivor區的內容都不再需要了,可以直接清空,到下一次GC時,兩個Survivor的角色再互換。因此,這種方式分配記憶體和清理記憶體的效率都極高,這種垃圾回收的方式就是著名的“停止-複製(Stop-and-copy)”清理法(將Eden區和一個Survivor中仍然存活的物件拷貝到另一個Survivor中),這不代表著停止複製清理法很高效,其實,它也只在這種情況下(基於大部分物件存活週期很短的事實)高效,如果在老年代採用停止複製,則是非常不合適的。
老年代儲存的物件比年輕代多得多,而且不乏大物件,對老年代進行記憶體清理時,如果使用停止-複製演算法,則相當低效。一般,老年代用的演算法是標記-壓縮演算法,即:標記出仍然存活的物件(存在引用的),將所有存活的物件向一端移動,以保證記憶體的連續。在發生Minor GC時,虛擬機器會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,如果大於,則直接觸發一次Full GC,否則,就檢視是否設定了-XX:+HandlePromotionFailure
(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍記憶體分配失敗;如果不允許,則仍然進行Full
GC(這代表著如果設定-XX:+Handle PromotionFailure
,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多記憶體,所以,最好不要這樣做)。
關於方法區即永久代的回收,永久代的回收有兩種:常量池中的常量,無用的類資訊,常量的回收很簡單,沒有引用了就可以被回收。對於無用的類進行回收,必須保證3點:
1. 類的所有例項都已經被回收
2. 載入類的ClassLoader已經被回收
3. 類物件的Class物件沒有被引用(即沒有通過反射引用該類的地方)
永久代的回收並不是必須的,可以通過引數來設定是否對類進行回收。