這篇文章的素材來自周志明的《深刻理解Java虛擬機》。
做爲Java開發人員,必定程度瞭解JVM虛擬機的的運做方式很是重要,本文就一些簡單的虛擬機的相關概念和運做機制展開我本身的學習過程。java
java虛擬機運行在受不一樣操做系統操縱的物理機上,不一樣的操做系統使用不一樣的底層方法來執行不一樣的操做,這些方法稱之爲本地方法:Native Method,本地方法通常執行的都是比較底層的操做,好比說IO、線程管理等,java方法則會執行的通常是相對高級的操做,好比說數邏運算,或者是調用底層的本地方法來完成底層任務。算法
java虛擬機的運行時數據區域將內存分紅了不一樣的部分協調完成java虛擬機的內存數據交互。數據結構
按照數據存儲過程的數據結構能夠大體分爲:多線程
棧區:併發
虛擬機棧:java虛擬機運行的java方法(java字節碼方法)構成的棧空間,這個空間在運行時存儲這些方法的局部變量表、操做棧、動態連接和方法出口;jvm
本地方法棧:本地方法在運行時存儲數據產生的棧區。工具
堆區:性能
java堆:對象的實例存儲在這個共享的堆空間裏,因爲佔有最大的和最有實際意義的空間,這個空間的GC過程時虛擬機運行的重點。學習
方法區:存儲虛擬機運行時加載的類信息、常量、靜態變量和即時編譯的代碼,所以能夠把這一部分考慮爲一個保存相對來講數據較爲固定的部分,常量和靜態變量在編譯時就肯定下來進入這部份內存,運行時類信息會直接加載到這部份內存,因此都是相對較早期進入內存的。spa
運行時常量池:不是全部的常量都是在編譯時就肯定下來進入內存的,仍然會有運行時才進入內存的常量,這部分常量通常是編譯時產生的一些固定信息,好比說翻譯出的引用等,直接在類加載的時候把它們存入運行時常量池有助於提升性能。
全部的內存區域的數據交互由程序計數器指導虛擬機完成複雜的邏輯步驟。
如何找到一個對象的實例:
Object obj = new Object();
在這個過程當中在虛擬機棧的局部變量表裏建立obj引用,在堆內存裏建立Object類的一個實例,最後就是把obj引用和這個對象實例關聯起來的問題了,另外,咱們須要知道的是,不是全部的實例都完整地保存了全部的類的信息,通常共有的或者靜態的類的數據將被保存在方法區中,獨有的實例數據纔會真的被保存在java堆裏,所以每一個引用必須同時找到關聯它的實例數據和類數據。針對這個問題,有兩個辦法來作:
I. 引用存儲的只是實例的句柄,句柄在堆的句柄池中,句柄中保存着到堆中真正實例的地址和到方法區中類數據的地址,這樣就能夠經過這個句柄能夠找到這些地址。
II. 引用存儲的就是實例在堆中的地址,而實例中是含有能夠定位類數據的地址的,也就是經過找到的實例地址能夠再去尋找它對應的類的數據。
兩個和內存溢出相關的異常:
StackOverflowError:線程申請的棧深度大於虛擬機的規定值;
OutOfMemoryError:線程擴展增長的內存大於虛擬機的要求;
虛擬機棧、本地方法棧和計數器大都是編譯期肯定的內存分配,在線程執行完畢後即會清理,內存回收相對比較容易。因此咱們提到的內存回收大都是指堆內存的回收。咱們經過以下幾個問題來講明內存回收機制:
什麼樣的堆內存是能夠回收的呢?簡而言之就是那些「沒用」的內存,那麼怎樣的內存是「沒用」的呢?即那些經過現有的指針(或稱「引用」)條件下再也訪問不到的內存對象。因此有這樣的算法來描述無效的引用:
(引用計數算法)每一個對象都有一個被引用計數器,被引用一次計數器加1,引用被置空時減1,最終被引用計數器的值爲0 的便是「無用」的內存對象,它佔用的內存能夠被回收。
(這個算法看起來好像沒有問題,可是遭遇到循環引用的時候就會出現問題:若是同時將循環引用的雙方置空,那麼即便被引用計數器不爲0也再也訪問不到這些對象了,即發生了內存無端佔用)。
這個過程體現了互相循環引用可能帶來的問題,對象仍被引用可是已經不能被訪問了,因此是這種算法的缺陷。
(根搜索算法)將由棧內存或方法區引用的對象做爲GCRoots去構建引用鏈,若是能找到這個對象則說明這個對象可以訪問其內存不能被回收,反之經過這些引用鏈找不到這個對象則說明已是棄用的對象了,其內存是應該被回收的。(上面的互相循環引用的例子就能夠解決了,由於這個問題裏面雖然其被引用計數器的值不爲0,可是已經沒有GCRoots可以找到這些內存了,這個問題裏的GC Roots是棧內存裏的objA和objB,這兩個棧內存裏的引用被置空,所以引用鏈裏沒辦法再找到對內存裏的對象了。)
肯定了有哪些內存該被回收後GC機制是直接回收內存嗎?GC會給這些內存中的某些對象一次機會,就是那些重寫過finalize方法的類的對象,GC會執行這個對象重寫過的finalize方法,若是在這個方法中對象從新將本身連接給了某個引用使得這塊內存區域從新能夠被訪問,那麼GC就不會在此次回收它,可是,這個過程只能執行一次,下一次再被GC遇到的話就不會顧及這個finalize方法而是直接回收了,所以要注意重寫的finalize方法只能執行一次。
這個是堆內存中對象的回收,在方法區裏保存類信息和常量池的內存一樣須要回收,這個過程相對來講更緩慢也並無那麼高效,由於一段時間內線程使用的類和常量池都比較穩定,只有當真的確認有類再也不使用且不被反射使用的時候纔會卸載類,當真的沒有常量再被使用的時候纔會釋放常量池中不用的常量。
知道了哪些內存該被回收、回收前的最後確認以後來講內存回收策略,也就是內存回收的時候到底是依據什麼樣的算法進行的?
(標記-清除算法)
(複製算法)
(標記-整理算法)
經過這些算法,jvm能夠將已不被引用的無效內存回收,標記-清除算法清理獲得的內存每每出現碎片,而標記-整理解決了內存碎片卻增長了時間消耗,複製算法則會出現內存浪費的問題,結合不一樣場景使用不一樣算法進行垃圾回收是十分重要的。
瞭解了內存垃圾回收的算法,咱們來看執行垃圾回收的垃圾收集器。根據堆內存對對象的代的劃分咱們對堆內存有這樣劃分:
各版本和種類的垃圾回收器各有其用武之地,配合使用它們獲得最好的效果十分重要。由於在垃圾內存回收的過程當中對每一個對象分代處理,因此對不一樣代的垃圾內存有不一樣的收集器去回收:建立不久的對象稱爲新生代,新生代對象的特色便是生死頻率高,從生到死的過程很短,因此再回收時有大量的這樣的內存存在,因此採用複製算法採用較大的eden:survivor比率將使得內存較完整也較快地回收,同時,老年代的內存存儲的是建立好久仍然沒有失去引用的對象,這類對象因爲長期存在於內存中且將來的生死也經常不肯定,因此須要使用速度慢可是更精確地標記-整理算法。下面是真正執行這些回收過程的收集器:
新生代收集器:(主要使用複製算法)
Serial收集器:單線程+「Stop the World」停頓式收集
ParNew收集器:多線程版本的Serial收集器
Parallel Scavenge收集器:多線程收集器,關注「吞吐量」
老年代收集器:(主要使用標記-整理算法)
Serial Old收集器:Serial的老年代版本
Parallel Old收集器:Parallel的老年代版本
CMS收集器:併發收集、低停頓,關注短期停頓
G1收集器:高級和領先的新型垃圾收集器
JVM虛擬機將會依次對每次即將進入堆內存的對象作出安排,必定時間間隔內對於失去引用的無效內存進行回收,當內存出現溢出的時候試圖經過垃圾回收自發解決問題保持系統迴歸平穩。
申請內存的對象優先被分配到堆內存的Eden區,若是Eden區的空間不足就向survivor區上放,若是仍然放不下就會引起一次發生在新生代的minor GC,在此次GC過程當中,若是發現仍然又放不下的對象,就將這些對象放入老年代內存裏去(這種現象是對垃圾回收的統計學規律的挑戰,由於理論上大多數新生代內存不該該存活到這個時候,因此這個時候就會引起這種叫作分配擔保機制的對象向老年代轉移),若是存在失去引用的內存,那麼就將剩餘存活的對象移往survivor區,剩下的Eden區內存所有清理。
大對象直接進入老年區,上面的描述中咱們已經能夠看到大的對象在一旦出現長時間存活的時候會引起分配擔保機制進入老年區,因此不如直接在剛開始建立這個對象的時候就把它放入老年區。
長期存活的對象直接進入老年區:同上面的描述,長期存活的對象的移動會耗費資源,因此在建立這些長期存活的對象時就將它直接放入老年區。
動態對象的年齡判斷:虛擬機並非一直等待全部的對象都到達老年代的標準纔將它們放入老年期,由於那樣作可能會使新生代的空間一直很緊張引起沒必要要的GC,因此在當Survivor區裏的對象中相同年齡的對象的大小達到Survivor區的一半時就能夠將其移入老年區。
空間分配擔保:當每次執行minor GC的時候應該對要晉升到老年代的對象進行分析,若是這些立刻要到老年區的老年對象的大小超過了老年區的剩餘大小,那麼執行一次Full GC以儘量地得到老年區的空間。
這裏咱們使用一個實例藉助VusualVM來查看程序運行過程當中的虛擬機內存分配的過程:
在這個例子中,各類參數均使用默認值:
public class VMTest { private static final int _1MB = 1024*1024; public static void main(String[] args) throws InterruptedException { Thread.sleep(4000); byte[] allocation1; for (int i = 0; i < 400; i++) { allocation1 = new byte[_1MB]; System.out.println("Create One"+i); Thread.sleep(1000); } } }
這個例子中,主線程每次循環向虛擬機申請內存建立新對象,而後在循環結束的時候將引用連接到新的對象,原來的對象就會處於失去引用的狀態,每隔一段時間後JVM的minor GC就會使得這些棄用的對象佔據的內存被回收。如下便是這個過程當中VisualVM展現的的實時內存各區佔據狀況:
這個過程當中,咱們能夠清楚地看出內存分配的全過程。新的對象做爲新生代對象會被分配到新生區的Eden區中,在一個循環中這些對象都會被分配到Eden區中,由於Eden區默認的超過600M的空間足夠容納這些對象,當一段時間後發生minor GC的時候就會將仍然存活的(也就是仍然有有效引用的)對象移至空的Survivor區,在這裏是Survivor0區,失去引用的對象佔據的Eden區空間將會被回收;下一次monor GC到來以前仍然會進行這樣的空間分配,Eden區中會產生新的對象並有一些對象會失去有效引用,下一次minor GC到來的時候會把Eden區中存活的對象(以及Survivor0中存活的對象)移至空的Survivor區中,這裏是Survivor1,並將Eden和Survivor0回收。注意,每次minor GC進行的時候都會將一個Survivor(from Space)置空,並將存活的對象移至空Survivor(to Space)裏,若是Survivor(to Space)空間不足,則會引起分配擔保機制將這些存活對象移至老年區。