筆者最近在複習JVM的知識,本着記錄分享的精神,整理下學習Java虛擬機垃圾回收相關知識點,因爲整個垃圾回收內容比較多,我將整理成上下兩篇文章去分享,上篇我會主要分享Java虛擬機的運行時數據區域劃分,垃圾回收算法。下篇文章主要分享Java虛擬機的垃圾回收器以及一些虛擬機調優建議。html
Java虛擬機定義了程序在運行期間的多種數據區域,其中有些區域是在Java虛擬機建立的時候就建立了,只有在虛擬機退出後纔會被銷燬。根據Java虛擬機定義,咱們能夠數據區域作以下區分,分爲:堆、Java虛擬機棧、程序計數器、方法區(元數據區、運行時常量池、本地方法棧。下面咱們來詳細介紹下每一個區域的做用。java
程序計數器是一塊線程私有的區域,是一個較小的內存塊,用來存放當前線程執行的字節碼的指令地址,若是執行的是本地方法(Native),這個計數器就會爲空(Undefined)。算法
Java虛擬機棧是線程私有的區域,生命週期與線程相同,它存儲的是棧幀(Stack Frame),棧幀會來存儲局部變量表、操做數棧、動態連接、方法出口和返回地址等信息。每個方法從調用到執行完成的過程,都對應着一個棧幀在虛擬機棧中入棧到出棧的過程。若是線程請求的棧深度大於虛擬機所容許的最大深度,就會拋出StackOverflowError異常;若是申請棧內存不夠,也會致使拋出OutOfMemoryError異常。數組
Jvm參數
-Xss:棧空間大小;棧的空間大小決定了棧能建立的深度
複製代碼
棧結構以下:數據結構
本地方法棧和java方法棧很是相似,他們以前的區別主要是Java方法棧是提供給字節碼服務的,本地方法棧是給本地方法(C語言實現)調用服務的。Java虛擬機並無對本地方法棧中使用的語言、數據結構等進行強制規定,因此虛擬機能夠自行實現它。Sun HotSpot虛擬機把虛擬機棧和Java方法棧進行了合二爲一。本地方法棧也會和虛擬機棧同樣拋出StackOverFlowError和OutOfMemoryError異常。oracle
Java堆是一個全部線程共享的區域,堆用來存儲幾乎全部對象的實例和數組,堆按照分代的思想進行劃分,能夠劃分了新生代(YoungGeneration)和老年代(Old/Tenured Generation),新生代又可進一步細分爲 eden、survivor space0(s0 或者 from space)和 survivor space1(s1或者to space)。咱們用圖來表示下堆的劃分:jvm
eden區:新建對象通常都放在該區域,除非是新建了大對象,該區域放不下就直接存放在老年代(Tenured)。 S0和S1區:該區域放置的對象至少經歷了一次垃圾回收(Minor GC),若是經歷了屢次回收,到達指定次數還存活,那麼就會被轉移到老年代。ide
Java虛擬機規範規定堆能夠是物理上不連續的空間,只須要邏輯上連續便可,咱們能夠經過命令(-Xmx和-Xms )來調整堆空間,若是申請的堆內存超過了堆的最大內存,將會拋出OutOfMemoryError異常。學習
Jvm參數
-Xmx:最大堆空間大小
-Xms:最小堆空間大小
-Xmn:新生代空間大小
複製代碼
方法區是線程共享的區域,它用於存放已經被虛擬機加載的類信息、常量池、靜態變量、即時編譯器編譯後的代碼等數據。類信息包括類的完整名稱、父類的完整名稱、類型修飾符(public/protected/private)和類型的直接接口類表;常量池指運行時常量池(後面有介紹);方法區又被稱爲非堆(Non-Heap)。spa
在Host Spot虛擬機的實現中,方法區也被稱爲永久區,是一塊獨立於 Java 堆的內存空間。雖然叫永久區,可是永久區中的對象一樣能夠被 GC 回收的(注:方法區是 JVM 的一種規範,永久區是一種具體實現,在 Java8 中,永久區已經被 Metaspace 元空間取而代之。相應的,JVM參數 PermSize 和 MaxPermSize 被 MetaSpaceSize 和 MaxMetaSpaceSize 取代
)。對永久區 GC 的回收,一般主要從兩個方面分析:一是 GC 對永久區常量池的回收;二是永久區對類元數據的回收。
當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
運行時常量池(Run-Time Constant Pool)是方法區的一部分,它主要用來存放編譯期生成的各類字面量和符號引用,既然是運行時常量池,理所應當的能夠存放運行時產生的常量,好比調用String.intern()
方法產生的字符串常量就會被放入運行池常量中。
引用計數法的思想比較簡單,每一個對象都有一個引用計數器,只要對象被引用,計數器就+1,當對象再也不被引用時候,計數器就減一。這種算法很高效,可是有一個致命缺點,就是有循環引用的問題。對於兩個無用對象的互相引用,就會致使兩個對象的計數器不爲0,從而沒法被斷定爲無用對象,沒法回收內存。
因爲引用計數法有互相引用的缺陷,因此Java虛擬機採用了可達性分析算法來斷定垃圾對象。這個算法的思想是,以一系列稱爲「GC Roots」的對象做爲起始點,從這些起點往下搜索,搜索所走過的路徑稱爲引用鏈(Referenc Chain),當一個對象到GC Roots沒有任何引用鏈(從GC Roots到這個對象不可達)時,就說明這個對象不可用,能夠被回收。
能夠做爲GC Roots的對象包括:
那爲何上面四種對象就能夠做爲GC Roots呢?
1.虛擬機棧中當前引用的對象,由於虛擬機棧中的對象是隨着線程的生命週期存活的,那麼在垃圾判斷的時候,當前線程還存活,也就意味着棧中持有的對象確定是存活的,因此能夠做爲GC Roots,本地方法棧也是同樣的道理。
2.對於方法區中的靜態變量引用和常量,個人理解是使用方法區中的對象做爲GC Roots並非必定就會以裏面全部的對象做爲GC Roots,雖然Java虛擬機並無規定方法區要進行回收,可是該區域在目前的JVM實現中都有回收,因爲方法區也會對「廢棄常量」和「無用類」進行回收,因此選擇GC Roots只會選擇方法區內的有效對象。"廢棄常量"判斷比較簡單,對於「無用類」的判斷,Java虛擬機只會判斷動態加載的類,對於原始加載的類,虛擬機永遠不會自動卸載。因此判斷動態加載的類爲無用類能夠有如下原則:
標記-清除算法分爲「標記」和「清除」兩個階段,首先須要標記出須要回收的對象,標記完成後再進行統一的垃圾回收。該算法有兩個缺點:1.效率不高;2.清除後會產生大量不連續的內存碎片,內存碎片會致使分配大對象時候,沒法找到足夠的內存,從而提早觸發一次GC.
上面的標記清除算法效率不高,爲了解決這個問題,就有了複製算法,複製算法就是把內存容量劃分爲大小相等的兩塊,每次只用其中一塊,當一塊內存用完後就將存活的對象複製到另一塊內存上,而後再對原內存塊進行清理。這種算法的優勢就是內存分配不用考慮碎片的問題,只須要移動堆頂的內存指針,按順序分配內存便可。可是這算法的缺點就是空間利用率不高,將內存縮小爲原來的一半,有一半的內存沒有被真正利用起來。
雖然內存利用率不高,可是目前的虛擬機中堆中的新生代就是採用這種算法進行垃圾回收的。上面咱們提到新生代分爲 eden 空間、form 空間和 to空間3個部分。其中 from 和 to 空間能夠視爲用於複製的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱爲 survivor 空間,即倖存者空間,用於存放未被回收的對象。
在垃圾回收時,eden空間中存活的對象會被複制到未使用的survivor空間中(假設是 to),正在使用的survivor空間(假設是 from)中的年輕對象也會被複制到to空間中(大對象或者老年對象會直接進入老年代,若是to空間已滿,則對象也會進入老年代)。此時eden和from空間中剩餘對象就是垃圾對象,直接清空,to空間則存放這次回收後存活下來的對象。
複製算法只適用於存活率較低的新生代中,若是存活率較高就須要進行過多的複製操做,效率將會下降。老年代的存活率比較高,因此複製算法不適用於老年代的場景,以前提到的「標記-清除」算法,若是不會產生內存碎片的話,仍是能夠知足老年代的,那麼有沒有不產生碎片的相似算法呢?答案是有,「標記-整理」算法就派上用處了。它的核心思想是:先對可回收對象進行標記,而後把全部存活的對象移動到一端,接着直接清理掉邊界意外的內存區域。由於清理事後,存活對象都緊密的在一端,因此不會產生內存碎片。
本篇文章我整理了Java虛擬機的運行區劃分,每一個區域的做用,同時分享了垃圾判斷算法和垃圾回收算法。運行時數據區劃分爲:程序計數器、Java虛擬機棧、本地方法棧、堆、方法區、運行時常量池。有的文章中提到Jdk1.7及之後的版本把運行時常量從方法區移除,這裏我想說明下,Java虛擬機規範仍是要求在方法區分配,這只是個別虛擬機的本身實現,好比說Hot Spot虛擬機。
垃圾斷定算法如今虛擬機主要使用可達性分析算法,垃圾回收算法有「標記-清除」算法、「複製」算法、「標記-整理」算法。「複製」算法比較適合存活對象較少的新生代,「標記-整理」算法比較適合老年代,整理的做用就是爲了有連續的內存空間,防止內存碎片太多沒法存放大對象。
《深刻理解jvm虛擬機 第二版》