理解Java內存區域與垃圾收集器

本文目錄結構html

  • java內存區域
    • 運行時內存區域
    • 對象訪問
  • 垃圾收集器
    • 判斷對象死亡
    • 方法區回收
    • GC回收算法
    • 空間分配擔保
  • 參考

java內存區域

運行時內存區域

java虛擬機在執行java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。 java

java內存區域
咱們注意到運行時區域主要會包括5部分區域,它們有個各自的用途,以及建立和銷燬時間,有的依賴虛擬機進程,有的依賴用戶線程。

  • 程序計數器 程序計數器是一塊較小的內存空間,它的做用是當前線程所執行到的字節碼的位置指示器。字節碼解釋器工做時就是經過改變計數器的值來選取下一條須要執行的字節碼指令,從而達到分支、循環、跳轉、異常處理等基本功能。 java虛擬機中的多線程實際是經過線程輪流切換實現的。因此實際上在同一時刻,處理器的一個內核只會執行一條指令。所以爲了線程切換後還能恢復到正確的執行位置,須要每一個線程都要有一個獨立的程序計數器。並且他們之間互補影響,獨立工做。因此程序計數器是一塊線程私有的內存。
  • 本地方法棧 與程序計數器同樣,本地方法棧也是線程私有的。本地方法棧爲虛擬機提供使用Native方法服務。因爲虛擬機規範並無對本地方法棧中的使用語言和數據結構等作強制規定,因此虛擬機能夠自由實現。
  • java虛擬機棧 同本地方法棧,虛擬機棧是線程私有,它的生命週期與當前線程相同。它爲虛擬機執行java方法提供服務。它描述的內存模型:每一個方法被執行的時候會同時建立一個棧楨,用於存儲局部變量、操做棧、動態連接、方法出口等信息。每一個方法的調用到返回結果的過程,就是對應一個棧楨的入棧與出棧。 常常有人會說java內存能夠粗糙的區分爲堆和棧,這裏的棧就是虛擬機棧,而虛擬機棧中最重要的就是局部變量表。 局部變量表存放了編譯期可知的基本數據類型、對象的引用(reference類型,它可能只想對象起始地址的引用指針,也可能指向表明改對象的句柄)。局部變量表所須要的內存在編譯期完成分配,當進入一個方法時,此方法所須要的內存空間大小是肯定的,因此在方法運行期間,不會改變局部變量表的大小。
  • java堆 對於虛擬機來講,堆是其所管理的最大的一塊內存。java堆是指被線程共享的一塊內存區域,它在虛擬機啓動時即建立,堆的惟一目的時存放對象實例。同時因爲堆空間有限,對象的建立和銷燬是時常發生的,因此java堆是垃圾收集器的主要管理區域,因此java堆有時也會稱爲GC堆。如今的GC回收基本都採用分代回收算法,因此堆能夠細分爲新生代和老年代,新生代又能夠分爲eden區,from Survivor空間和to Survivor空間等。對於堆中的各個區域分配和回收細節,在GC部分講解。 在虛擬機規範中,沒有強制要求堆是物理內存連續的,只是邏輯上連續便可。因此當前的主流虛擬機的堆空間都是能夠動態擴容的,能夠經過-Xmx和-Xms控制。
  • 方法區 方法區同java堆都是線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。java虛擬機實現規範對該區域並無強制要求實現GC回收,因此相對而言,該區域的垃圾收集器不多出現,因此有人開發者會成稱該區域爲永久代。這個區域的內存回收主要是針對常量池的回收和對類型的卸載。 運行時常量池 一個class文件除了有類的版本、字段、方法、接口等描述之外,還有一項是常量池,用於存放編譯期間生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。運行時常量池是具備動態性的,java虛擬機對class文件的每一部分的格式有嚴格的規定,每一個字節用於存儲哪一種數據都有規範要求,這樣纔會被虛擬機承認。可是對於常量池是比較寬鬆的,由於java並不要求常量必定要編譯期產生,也能夠在運行期間放入常量,好比String的intern()方法。

對象訪問

在java虛擬機棧中咱們提到局部變量表存放了對象的引用,咱們都知道對象是分配的java堆中的,那麼具體是怎麼引用的呢? 好比Object obj = new Object();,假設這句代碼出如今方法體中,那麼「Object obj 」這部分語義將會反映到java棧的本地變量表中(爲reference類型),而「new Object()」這部分語義將會反映在java堆上,造成一塊存儲了Object類型全部實例數據值的結構化內存。 因爲reference類型在java虛擬機規範中只規定了一個指向對象的引用,因此在實際虛擬機中訪問會有所不一樣,主流訪問有兩種:git

  • 句柄訪問
  • 直接指針訪問
句柄訪問

java堆會劃分出一小塊內存空間做爲句柄池,reference中存儲的就是對象的句柄地址,二句柄中包含了對象實例數據和類型數據的各自地址信息。 github

image.png

直接地址訪問

reference中直接存儲的就是對象的地址,java堆須要考慮對象的佈局中如何存放訪問類型數據的相關信息。 算法

image.png
這兩種對象的訪問方式各有優點,使用句柄訪問方式的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而reference自己不須要被修改。使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的的時間開銷。

垃圾收集器

判斷對象死亡

GC在對堆內存進行回收前,第一件事是須要肯定哪些對象是須要被回收的,因此就須要判斷對象是否存活。通常的有兩種方法來判斷:markdown

  1. 引用計數法 給一個對象添加一個引用計數器,每當有地方對其引用時,計數器加1,當引用實效時,計數器減1,任什麼時候刻計數器爲0時就表示該對象再也不被使用。 引用計數法實現簡單,一般是比較高效的,可是引用計數法有個弊端是當兩個再也不被使用的對象互相引用時,致使二者都不會被釋放。
  2. 根搜索算法 根搜索算法是指經過一系列名爲「GC roots"的對象爲起點,從這些節點開始向下搜索,搜索過的路徑稱爲引用鏈,當一個對象到GC roots沒有任何引用鏈相連,就表示此對象再也不被使用。 在java語言中,做爲GC roots的對象包括如下幾種: a. java虛擬機棧(棧楨中本地變量表)中引用的對象 b. 方法區中類靜態屬性引用的對象 c. 方法區中常量引用的對象 d. 本地方法棧中JNI引用的對象

方法區回收

前面已經提到方法區是不多出現垃圾收集器的,由於方法區回收的性價比比較低,一般堆內存的回收一次能夠回收70%-95%的空間,但方法區的垃圾收集器效率很低。 通常的,方法區回收主要由兩部分: 1.廢棄常量 廢棄的常量與堆回收比較相似,只須要指導該常量是否在其餘地方被使用便可。 2.無用的類 這種狀況的判斷比較苛刻,通常要求知足如下三個條件纔算是無用的: a. 該類的全部實例都被回收 b. 加載該類的ClassLoader也被回收 c. 該類對應的java.lang.class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類數據結構

GC回收算法

1.標記-清除算法 最基礎的收集算法是「標記-清除」(Mark-Sweep)算法,如同它的名字同樣,算法分爲「標記」和「清除」兩個階段。 a. 首先標記出全部須要回收的對象 b. 在標記完成後統一回收全部被標記的對象。 缺點: 效率問題:標記和清除兩個過程的效率都不高 空間問題:標記清除以後產生大量不連續的內存碎片,空間碎片太多可能會致使之後程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。 多線程

標記-清除算法

2.複製算法 目的是爲了解決效率問題。 將可用內存按容量大小劃分爲大小相等的兩塊,每次只使用其中的一塊。當一塊內存使用完了,就將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況。 缺點: 將內存縮小爲了原來的一半。 oop

複製算法
現代的商業虛擬機都採用這種收集算法來回收新生代,IBM公司的專門研究代表,新生代中對象98%對象是「朝生夕死」的,因此不須要按照1:1的比例來劃份內存空間,而是將內存分爲較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。

3.標記-整理算法 複製收集算法在對象存活率較高時,就要進行較多的複製操做,效率就會變低。 根據老年代的特色,提出了「標記-整理」算法。 標記過程仍然與」標記-清除「算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存。 佈局

標記-整理算法

4.分代收集算法 通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法。在老年代中,由於對象存活率高、沒有額外空間對它進行分配擔保,就必須採用「標記-清除」或「標記-整理」算法來進行回收。JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(分別叫from和to),默認比例爲8:1。 工做過程:通常狀況下,新建立的對象都會被分配到Eden區(一些大對象特殊處理),這些對象通過第一次GC後,若是仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次GC,年齡就會增長1歲,當它的年齡增長到必定程度時,就會被移動到年老代中。 由於年輕代中的對象基本都是朝生夕死的(80%以上),因此在年輕代的垃圾回收算法使用的是複製算法,複製算法不會產生內存碎片。在GC開始的時候,對象只會存在於Eden區和名爲「From」的Survivor區,Survivor區「To」是空的。緊接着進行GC,Eden區中全部存活的對象都會被複制到「To」,而在「From」區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到「To」區域。通過此次GC後,Eden區和From區已經被清空。這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。無論怎樣,都會保證名爲To的Survivor區域是空的。GC會一直重複這樣的過程,直到「To」區被填滿,「To」區被填滿以後,會將全部對象移動到年老代中。

空間分配擔保

先了解下Minor GC與Major GC/Full GC

  • Minor GC 即新生代GC,指發生在新生代的垃圾收集動做,Minor GC的回收的對象大多具有朝生夕滅的特性,因此Minor GC是很是頻繁,而且回收速度比較快。
  • Major GC/Full GC 即老年代GC,指發生在老年代的垃圾收集動做,出現Major GC,常常會伴隨至少一次的Minor GC。Major GC的速度通常比Minor GC慢10倍以上。

在發生Minor GC時,虛擬機會檢測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,若是大於,則改成直接進行一次Full GC,若是小於,則查看HandlePromotionFailure設置是否容許擔保失敗,若是容許,那麼只會進行Minor GC,若是不容許,那麼進行一次Full GC。 在分代回收算法中提到過,新生代使用複製收集算法,但爲了內存利用率,只使用其中一個Survivor空間來做爲輪換備份,所以當出現大量對象在Minor GC後仍然存活的狀況(最極端的狀況就是內存回收後新生代中全部對象都存活),就須要老年代進行分配擔保,把Survivor沒法容納的對象直接進入老年代。與生活中的貸款擔保相似,老年代要進行這樣的擔保,前提是老年代自己還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成內存回收以前是沒法明確知道的,因此只好取以前每一次回收晉升到老年代對象容量的平均大小值做爲經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

參考

@Dpuntu, 本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。

相關文章
相關標籤/搜索