JVM的相關知識整理和學習

JVM是虛擬機,也是一種規範,他遵循着馮·諾依曼體系結構的設計原理。馮·諾依曼體系結構中,指出計算機處理的數據和指令都是二進制數,採用存儲程序方式不加區分的存儲在同一個存儲器裏,而且順序執行,指令由操做碼和地址碼組成,操做碼決定了操做類型和所操做的數的數字類型,地址碼則指出地址碼和操做數。從dos到window8,從unix到ubuntu和CentOS,還有MAC OS等等,不一樣的操做系統指令集以及數據結構都有着差別,而JVM經過在操做系統上創建虛擬機,本身定義出來的一套統一的數據結構和操做指令,把同一套語言翻譯給各大主流的操做系統,實現了跨平臺運行,能夠說JVM是java的核心,是java能夠一次編譯處處運行的本質所在。html

1、JVM的組成和運行原理

JVM的畢竟是個虛擬機,是一種規範,雖然說符合馮諾依曼的計算機設計理念,可是他並非實體計算機,因此他的組成也不是什麼存儲器,控制器,運算器,輸入輸出設備。在我看來,JVM放在運行在真實的操做系統中表現的更像應用或者說是進程,他的組成能夠理解爲JVM這個進程有哪些功能模塊,而這些功能模塊的運做能夠看作是JVM的運行原理。JVM有多種實現,例如Oracle的JVM,HP的JVM和IBM的JVM等,而在本文中研究學習的則是使用最普遍的Oracle的HotSpot JVM。java

1.JVM在JDK中的位置。

JDK是java開發的必備工具箱,JDK其中有一部分是JRE,JRE是JAVA運行環境,JVM則是JRE最核心的部分。我從oracle.com截取了一張關於JDK Standard Edtion的組成圖,算法

輸入圖片說明

從最底層的位置能夠看出來JVM有多重要,而實際項目中JAVA應用的性能優化,OOM等異常的處理最終都得從JVM這兒來解決。HotSpot是Oracle關於JVM的商標,區別於IBM,HP等廠商開發的JVM。Java HotSpot Client VM和Java HotSpot Server VM是JDK關於JVM的兩種不一樣的實現,前者能夠減小啓動時間和內存佔用,然後者則提供更加優秀的程序運行速度(參考自:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/index.html ,該文檔有關於各個版本的JVM的介紹)。在命令行,經過java -version能夠查看關於當前機器JVM的信息,下面是我在Win8系統上執行命令的截圖,ubuntu

輸入圖片說明

能夠看出我裝的是build 20.13-b02版本,HotSpot 類型Server模式的JVM。數組

2.JVM的組成

JVM由4大部分組成:ClassLoader,Runtime Data Area,Execution Engine,Native Interface。 我從CSDN找了一張描述JVM大體結構的圖:性能優化

輸入圖片說明

2.1.ClassLoader是負責加載class文件,class文件在文件開頭有特定的文件標示,而且ClassLoader只負責class文件的加載,至於它是否能夠運行,則由Execution Engine決定。網絡

2.2.Native Interface是負責調用本地接口的。他的做用是調用不一樣語言的接口給JAVA用,他會在Native Method Stack中記錄對應的本地方法,而後調用該方法時就經過Execution Engine加載對應的本地lib。本來多於用一些專業領域,如JAVA驅動,地圖製做引擎等,如今關於這種本地方法接口的調用已經被相似於Socket通訊,WebService等方式取代。數據結構

2.3.Execution Engine是執行引擎,也叫Interpreter。Class文件被加載後,會把指令和數據信息放入內存中,Execution Engine則負責把這些命令解釋給操做系統。多線程

2.4.Runtime Data Area則是存放數據的,分爲五部分:Stack,Heap,Method Area,PC Register,Native Method Stack。幾乎全部的關於java內存方面的問題,都是集中在這塊。下圖是javapapers.com上關於Run-time Data Areas的描述:架構

輸入圖片說明

能夠看出它把Method Area化爲了Heap的一部分,javapapers.com中認爲Method Area是Heap的邏輯區域,但這取決於JVM的實現者,而HotSpot JVM中把Method Area劃分爲非堆內存,顯然是不包含在Heap中的。下圖是javacodegeeks.com中,2014年9月刊出的一片博文中關於Runtime Data Area的劃分,其中指出,NonHeap包含PermGen和Code Cache,PermGen包含Method Area,並且PermGen在JAVA SE 8中已經再也不用了。查閱資料(https://abhirockzz.wordpress.com/2014/03/18/java-se-8-is-knocking-are-you-there/)得知,java8中PermGen已經從JVM中移除並被MetaSpace取代,java8中也不會見到OOM:PermGen Space的異常。目前Runtime Data Area能夠用下圖描述它的組成:

輸入圖片說明

2.4.1.Stack是java棧內存,它等價於C語言中的棧,棧的內存地址是不連續的,每一個線程都擁有本身的棧。棧裏面存儲着的是StackFrame,在《JVM Specification》中文版中被譯做java虛擬機框架,也叫作棧幀。StackFrame包含三類信息:局部變量,執行環境,操做數棧。局部變量用來存儲一個類的方法中所用到的局部變量。執行環境用於保存解析器對於java字節碼進行解釋過程當中須要的信息,包括:上次調用的方法、局部變量指針和操做數棧的棧頂和棧底指針。操做數棧用於存儲運算所須要的操做數和結果。StackFrame在方法被調用時建立,在某個線程中,某個時間點上,只有一個框架是活躍的,該框架被稱爲Current Frame,而框架中的方法被稱爲Current Method,其中定義的類爲Current Class。局部變量和操做數棧上的操做老是引用當前框架。當Stack Frame中方法被執行完以後,或者調用別的StackFrame中的方法時,則當前棧變爲另一個StackFrame。Stack的大小是由兩種類型,固定和動態的,動態類型的棧能夠按照線程的須要分配。 下面兩張圖是關於棧之間關係以及棧和非堆內存的關係基本描述(來自http://www.programering.com/a/MzM3QzNwATA.html):

輸入圖片說明

輸入圖片說明

2.4.2.Heap是用來存放對象信息的,和Stack不一樣,Stack表明着一種運行時的狀態。換句話說,棧是運行時單位,解決程序該如何執行的問題,而堆是存儲的單位,解決數據存儲的問題。Heap是伴隨着JVM的啓動而建立,負責存儲全部對象實例和數組的。堆的存儲空間和棧同樣是不須要連續的,它分爲Young Generation和Old Generation(也叫Tenured Generation)兩大部分。Young Generation分爲Eden和Survivor,Survivor又分爲From Space和 ToSpace。

和Heap常常一塊兒說起的概念是PermanentSpace,它是用來加載類對象的專門的內存區,是非堆內存,和Heap一塊兒組成JAVA內存,它包含MethodArea區(在沒有CodeCache的HotSpotJVM實現裏,則MethodArea就至關於GenerationSpace)。在JVM初始化的時候,咱們能夠經過參數來分別指定,PermanentSpace的大小、堆的大小、以及Young Generation和Old Generation的比值、Eden區和From Space的比值,從而來細粒度的適應不一樣JAVA應用的內存需求。

2.4.3.PC Register是程序計數寄存器,每一個JAVA線程都有一個單獨的PC Register,他是一個指針,由Execution Engine讀取下一條指令。若是該線程正在執行java方法,則PC Register存儲的是 正在被執行的指令的地址,若是是本地方法,PC Register的值沒有定義。PC寄存器很是小,只佔用一個字寬,能夠持有一個returnAdress或者特定平臺的一個指針。

2.4.4.Method Area在HotSpot JVM的實現中屬於非堆區,非堆區包括兩部分:Permanet Generation和Code Cache,而Method Area屬於Permanert Generation的一部分。Permanent Generation用來存儲類信息,好比說:class definitions,structures,methods, field, method (data and code) 和 constants。Code Cache用來存儲Compiled Code,即編譯好的本地代碼,在HotSpot JVM中經過JIT(Just In Time) Compiler生成,JIT是即時編譯器,他是爲了提升指令的執行效率,把字節碼文件編譯成本地機器代碼,以下圖:

輸入圖片說明

引用一個經典的案例來理解Stack,Heap和Method Area的劃分,就是Sring a=」xx」;Stirng b=」xx」,問是否a==b? 首先==符號是用來判斷兩個對象的引用地址是否相同,而在上面的題目中,a和b按理來講申請的是Stack中不一樣的地址,可是他們指向Method Area中Runtime Constant Pool的同一個地址,按照網上的解釋,在a賦值爲「xx」時,會在Runtime Contant Pool中生成一個String Constant,當b也賦值爲「xx」時,那麼會在常量池中查看是否存在值爲「xx」的常量,存在的話,則把b的指針也指向「xx」的地址,而不是新生成一個String Constant。我查閱了網絡上你們關於String Constant的存儲的說說法,存在略微差異的是,它存儲在哪裏,有人說Heap中會分配出一個常量池,用來存儲常量,全部線程共享它。而有人說常量池是Method Area的一部分,而Method Area屬於非堆內存,那怎麼能說常量池存在於堆中?

我認爲,其實兩種理解都沒錯。Method Area的確從邏輯上講能夠是Heap的一部分,在某些JVM實現裏從堆上開闢一塊存儲空間來記錄常量是符合JVM常量池設計目的的,因此前一種說法沒問題。對於後一種說法,HotSpot JVM的實現中的確是把方法區劃分爲了非堆內存,意思就是它不在堆上。我在HotSpot JVM作了個簡單的實驗,定義多個常量以後,程序拋出OOM:PermGen Space異常,印證了JVM實現中常量池是在Permanent Space中的說法。可是,個人JDK版本是1.6的。查閱資料,JDK1.7中InternedStrings已經再也不存儲在PermanentSpace中,而是放到了Heap中;JDK8中PermanentSpace已經被徹底移除,InternedStrings也被放到了MetaSpace中(若是出現內存溢出,會報OOM:MetaSpace,這裏有個關於二者性能對比的文章:http://blog.csdn.net/zhyhang/article/details/17246223 )。 因此,仁者見仁,智者見智,一個饅頭足以引起血案,就算是同一個商家的JVM,畢竟JDK版本在更新,或許正如StackOverFlow上大神們所說,對於理解JVM Runtime Data Area這一部分的劃分邏輯,仍是去看對應版本的JDK源碼比較靠譜,或者是參考不一樣的版本JVM Specification( http://docs.oracle.com/javase/specs/ )。

2.4.5.Native Method Stack是供本地方法(非java)使用的棧。每一個線程持有一個Native Method Stack。

3.JVM的運行原理簡介

Java 程序被javac工具編譯爲.class字節碼文件以後,咱們執行java命令,該class文件便被JVM的Class Loader加載,能夠看出JVM的啓動是經過JAVA Path下的java.exe或者java進行的。JVM的初始化、運行到結束大概包括這麼幾步:

調用操做系統API判斷系統的CPU架構,根據對應CPU類型尋找位於JRE目錄下的/lib/jvm.cfg文件,而後經過該配置文件找到對應的jvm.dll文件(若是咱們參數中有-server或者-client, 則加載對應參數所指定的jvm.dll,啓動指定類型的JVM),初始化jvm.dll而且掛接到JNIENV結構的實例上,以後就能夠經過JNIENV實例裝載而且處理class文件了。class文件是字節碼文件,它按照JVM的規範,定義了變量,方法等的詳細信息,JVM管理而且分配對應的內存來執行程序,同時管理垃圾回收。直到程序結束,一種狀況是JVM的全部非守護線程中止,一種狀況是程序調用System.exit(),JVM的生命週期也結束。

關於JVM如何管理分配內存,我經過class文件和垃圾回收兩部分進行了學習。

2、JVM的內存管理和垃圾回收

JVM中的內存管理主要是指JVM對於Heap的管理,這是由於Stack,PC Register和Native Method Stack都是和線程同樣的生命週期,在線程結束時天然能夠被再次使用。雖說,Stack的管理不是重點,可是也不是徹底不講究的。

1.棧的管理

JVM容許棧的大小是固定的或者是動態變化的。在Oracle的關於參數設置的官方文檔中有關於Stack的設置(http://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/jrdocs/refman/optionX.html#wp1024112),是經過-Xss來設置其大小。關於Stack的默認大小對於不一樣機器有不一樣的大小,而且不一樣廠商或者版本號的jvm的實現其大小也不一樣,以下表是HotSpot的默認大小:

輸入圖片說明

咱們通常經過減小常量,參數的個數來減小棧的增加,在程序設計時,咱們把一些常量定義到一個對象中,而後來引用他們能夠體現這一點。另外,少用遞歸調用也能夠減小棧的佔用。 棧是不須要垃圾回收的,儘管說垃圾回收是java內存管理的一個很熱的話題,棧中的對象若是用垃圾回收的觀點來看,他永遠是live狀態,是能夠reachable的,因此也不須要回收,他佔有的空間隨着Thread的結束而釋放。(參考自:http://stackoverflow.com/questions/20030120/java-default-stack-size)

關於棧通常會發生如下兩種異常:

1.當線程中的計算所須要的棧超過所容許大小時,會拋出StackOverflowError。

2.當Java棧試圖擴展時,沒有足夠的存儲器來實現擴展,JVM會報OutOfMemoryError。 我針對棧進行了實驗,因爲遞歸的調用能夠導致棧的引用增長,致使溢出,因此設計代碼以下: 輸入圖片說明

個人機器是x86_64系統,因此Stack的默認大小是128KB,上述程序在運行時會報錯: 輸入圖片說明 而當我在eclipse中調整了-Xss參數到3M以後,該異常消失。

輸入圖片說明

另外棧上有一點得注意的是,對於本地代碼調用,可能會在棧中申請內存,好比C調用malloc(),而這種狀況下,GC是管不着的,須要咱們在程序中,手動管理棧內存,使用free()方法釋放內存。

2.堆的管理

堆的管理要比棧管理複雜的多,我經過堆的各部分的做用、設置,以及各部分可能發生的異常,以及如何避免各部分異常進行了學習。 輸入圖片說明

上圖是 Heap和PermanentSapce的組合圖,其中 Eden區裏面存着是新生的對象,From Space和To Space中存放着是每次垃圾回收後存活下來的對象 ,因此每次垃圾回收後,Eden區會被清空。 存活下來的對象先是放到From Space,當From Space滿了以後移動到To Space。當To Space滿了以後移動到Old Space。Survivor的兩個區是對稱的,沒前後關係,因此同一個區中可能同時存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor複製過來的對象。並且,Survivor區總有一個是空的。同時,根據程序須要,Survivor區是能夠配置爲多個的(多於兩個),這樣能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能。

Old Space中則存放生命週期比較長的對象,並且有些比較大的新生對象也放在Old Space中。

堆的大小經過-Xms和-Xmx來指定最小值和最大值,經過-Xmn來指定Young Generation的大小(一些老版本也用-XX:NewSize指定), 即上圖中的Eden加FromSpace和ToSpace的總大小。而後經過-XX:NewRatio來指定Eden區的大小,在Xms和Xmx相等的狀況下,該參數不須要設置。經過-XX:SurvivorRatio來設置Eden和一個Survivor區的比值。(參考自博文:http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html)

堆異常分爲兩種,一種是Out of Memory(OOM),一種是Memory Leak(ML)。Memory Leak最終將致使OOM。實際應用中表現爲:從Console看,內存監控曲線一直在頂部,程序響應慢,從線程看,大部分的線程在進行GC,佔用比較多的CPU,最終程序異常終止,報OOM。OOM發生的時間不定,有短的一個小時,有長的10天一個月的。關於異常的處理,肯定OOM/ML異常後,必定要注意保護現場,能夠dump heap,若是沒有現場則開啓GCFlag收集垃圾回收日誌,而後進行分析,肯定問題所在。若是問題不是ML的話,通常經過增長Heap,增長物理內存來解決問題,是的話,就修改程序邏輯。

3.垃圾回收

JVM中會在如下狀況觸發回收:對象沒有被引用,做用域發生未捕捉異常,程序正常執行完畢,程序執行了System.exit(),程序發生意外終止。

JVM中標記垃圾使用的算法是一種根搜索算法。簡單的說,就是從一個叫GC Roots的對象開始,向下搜索,若是一個對象不能達到GC Roots對象的時候,說明它能夠被回收了。這種算法比一種叫作引用計數法的垃圾標記算法要好,由於它避免了當兩個對象啊互相引用時沒法被回收的現象。

1.標記清除算法,該算法是從根集合掃描整個空間,標記存活的對象,而後在掃描整個空間對沒有被標記的對象進行回收,這種算法在存活對象較多時比較高效,但會產生內存碎片。

2.複製算法,該算法是從根集合掃描,並將存活的對象複製到新的空間,這種算法在存活對象少時比較高效。

3.標記整理算法,標記整理算法和標記清除算法同樣都會掃描並標記存活對象,在回收未標記對象的同時會整理被標記的對象,解決了內存碎片的問題。

JVM中,不一樣的 內存區域做用和性質不同,使用的垃圾回收算法也不同,因此JVM中又定義了幾種不一樣的垃圾回收器(圖中連線表明兩個回收器能夠同時使用):

輸入圖片說明

1.Serial GC。從名字上看,串行GC意味着是一種單線程的,因此它要求收集的時候全部的線程暫停。這對於高性能的應用是不合理的,因此串行GC通常用於Client模式的JVM中。

2.ParNew GC。是在SerialGC的基礎上,增長了多線程機制。可是若是機器是單CPU的,這種收集器是比SerialGC效率低的。

3.Parrallel Scavenge GC。這種收集器又叫吞吐量優先收集器,而吞吐量=程序運行時間/(JVM執行回收的時間+程序運行時間),假設程序運行了100分鐘,JVM的垃圾回收佔用1分鐘,那麼吞吐量就是99%。Parallel Scavenge GC因爲能夠提供比較不錯的吞吐量,因此被做爲了server模式JVM的默認配置。

4.ParallelOld是老生代並行收集器的一種,使用了標記整理算法,是JDK1.6中引進的,在以前老生代只能使用串行回收收集器。

5.Serial Old是老生代client模式下的默認收集器,單線程執行,同時也做爲CMS收集器失敗後的備用收集器。

6.CMS又稱響應時間優先回收器,使用標記清除算法。他的回收線程數爲(CPU核心數+3)/4,因此當CPU核心數爲2時比較高效些。CMS分爲4個過程:初始標記、併發標記、從新標記、併發清除。

7.GarbageFirst(G1)。比較特殊的是G1回收器既能夠回收Young Generation,也能夠回收Tenured Generation。它是在JDK6的某個版本中才引入的,性能比較高,同時注意了吞吐量和響應時間。

對於垃圾收集器的組合使用能夠經過下表中的參數指定: 輸入圖片說明

默認的GC種類能夠經過jvm.cfg或者經過jmap dump出heap來查看,通常咱們經過jstat -gcutil [pid] 1000能夠查看每秒gc的大致狀況,或者能夠在啓動參數中加入:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log來記錄GC日誌。

GC中有一種狀況叫作Full GC,如下幾種狀況會觸發Full GC:

1.Tenured Space空間不足以建立打的對象或者數組,會執行FullGC,而且當FullGC以後空間若是還不夠,那麼會OOM:java heap space。

2.Permanet Generation的大小不足,存放了太多的類信息,在非CMS狀況下回觸發FullGC。若是以後空間還不夠,會OOM:PermGen space。

3.CMS GC時出現promotion failed和concurrent mode failure時,也會觸發FullGC。promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入舊生代,而此時舊生代也放不下形成的;concurrent mode failure是在執行CMS GC的過程當中同時有對象要放入舊生代,而此時舊生代空間不足形成的。

4.判斷MinorGC後,要晉升到TenuredSpace的對象大小大於TenuredSpace的大小,也會觸發FullGC。

能夠看出,當FullGC頻繁發生時,必定是內存出問題了。

相關文章
相關標籤/搜索