Java虛擬機是整個java平臺的基石,是java技術實現硬件無關和操做系統無關的關鍵環節,是java語言生成極小體積的編譯代碼的運行平臺,是保護用戶機器免受惡意代碼侵襲的保護屏障。JVM是虛擬機,也是一種規範,他遵循着馮·諾依曼體系結構的設計原理。馮·諾依曼體系結構中,指出計算機處理的數據和指令都是二進制數,採用存儲程序方式不加區分的存儲在同一個存儲器裏,而且順序執行,指令由操做碼和地址碼組成,操做碼決定了操做類型和所操做的數的數字類型,地址碼則指出地址碼和操做數。從dos到window8,從unix到ubuntu和CentOS,還有MACOS等等,不一樣的操做系統指令集以及數據結構都有着差別,而JVM經過在操做系統上創建虛擬機,本身定義出來的一套統一的數據結構和操做指令,把同一套語言翻譯給各大主流的操做系統,實現了跨平臺運行,能夠說JVM是java的核心,是java能夠一次編譯處處運行的本質所在。html
1、JVM的組成和運行原理java
JVM的畢竟是個虛擬機,是一種規範,雖然說符合馮諾依曼的計算機設計理念,可是他並非實體計算機,因此他的組成也不是什麼存儲器,控制器,運算器,輸入輸出設備。在我看來,JVM放在運行在真實的操做系統中表現的更像應用或者說是進程,他的組成能夠理解爲JVM這個進程有哪些功能模塊,而這些功能模塊的運做能夠看作是JVM的運行原理。JVM有多種實現,例如Oracle的JVM,HP的JVM和IBM的JVM等,而在本文中研究學習的則是使用最普遍的Oracle的HotSpot JVM。程序員
1.JVM在JDK中的位置。web
JDK是java開發的必備工具箱,JDK其中有一部分是JRE,JRE是JAVA運行環境,JVM則是JRE最核心的部分。算法
從最底層的位置能夠看出來JVM有多重要,而實際項目中JAVA應用的性能優化,OOM等異常的處理最終都得從JVM這兒來解決。HotSpot是Oracle關於JVM的商標,區別於IBM,HP等廠商開發的JVM。Java HotSpot Client VM和Java HotSpot Server VM是JDK關於JVM的兩種不一樣的實現,前者能夠減小啓動時間和內存佔用,然後者則提供更加優秀的程序運行速度。sql
2.JVM的組成apache
JVM由4大部分組成:ClassLoader,Runtime Data Area,Execution Engine,Native Interface。ubuntu
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,PCRegister,Native Method Stack。幾乎全部的關於java內存方面的問題,都是集中在這塊。
能夠看出它把Method Area化爲了Heap的一部分,基於網上的資料大部分認爲Method Area是Heap的邏輯區域,但這取決於JVM的實現者,而HotSpot JVM中把Method Area劃分爲非堆內存,顯然是不包含在Heap中的。下圖是javacodegeeks.com中,2014年9月刊出的一片博文中關於RuntimeData Area的劃分,其中指出,NonHeap包含PermGen和Code Cache,PermGen包含MethodArea,並且PermGen在JAVA SE 8中已經再也不用了。查閱資料得知,java8中PermGen已經從JVM中移除並被MetaSpace取代,java8中也不會見到OOM:PermGen Space的異常。目前Runtime Data Area能夠用下圖描述它的組成:
2.4.1.Stack是java棧內存,它等價於C語言中的棧,棧的內存地址是不連續的,每一個線程都擁有本身的棧。棧裏面存儲着的是StackFrame,在《JVM Specification》中文版中被譯做java虛擬機框架,也叫作棧幀。棧幀包含三類信息:局部變量,執行環境,操做數棧。局部變量用來存儲一個類的方法中所用到的局部變量。執行環境用於保存解析器對於java字節碼進行解釋過程當中須要的信息,包括:上次調用的方法、局部變量指針和操做數棧的棧頂和棧底指針。操做數棧用於存儲運算所須要的操做數和結果,它被設計爲一個後進先出的棧。StackFrame在方法被調用時建立,在某個線程中,某個時間點上,只有一個框架是活躍的,該框架被稱爲Current Frame,而框架中的方法被稱爲Current Method,其中定義的類爲Current Class。局部變量和操做數棧上的操做老是引用當前框架。當StackFrame中方法被執行完以後,或者調用別的StackFrame中的方法時,則當前棧變爲另一個StackFrame。Stack的大小是由兩種類型,固定和動態的,動態類型的棧能夠按照線程的須要分配。
2.4.2.Heap是用來存放對象信息的,和Stack不一樣,Stack表明着一種運行時的狀態。換句話說,棧是運行時單位,解決程序該如何執行的問題,而堆是存儲的單位,解決數據存儲的問題。Heap是伴隨着JVM的啓動而建立,負責存儲全部對象實例和數組的。堆的存儲空間和棧同樣是不須要連續的,它分爲Young Generation和Old Generation(也叫Tenured Generation)兩大部分。YoungGeneration分爲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存儲的是正在執行的java指令操做碼(例如iadd、ladd等),主要它只存儲操做碼,當java指令執行時,會從PC Register讀取操做碼,而後在操做數棧中取對應的操做數,若是是本地方法,PC Register的值沒有定義。PC寄存器很是小,只佔用一個字寬,能夠持有一個returnAdress或者特定平臺的一個指針。
2.4.4.Method Area在HotSpot JVM的實現中屬於非堆區,非堆區包括兩部分:Permanet Generation和Code Cache,而Method Area屬於Permanert Generation的一部分。Permanent Generation用來存儲類信息,好比說:classdefinitions,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」;Stirngb=」xx」,問是否a==b? 首先==符號是用來判斷兩個對象的引用地址是否相同,而在上面的題目中,a和b按理來講申請的是Stack中不一樣的地址,可是他們指向Method Area中Runtime Constant Pool的同一個地址,按照網上的解釋,在a賦值爲「xx」時,會在RuntimeContant 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中的說法。JDK1.7中InternedStrings已經再也不存儲在PermanentSpace中,而是放到了Heap中;JDK8中PermanentSpace已經被徹底移除,InternedStrings也被放到了MetaSpace中(若是出現內存溢出,會報OOM:MetaSpace)。
因此在oracle hotspot 1.7中,PermGen Space是非堆內存,方法區屬於PermGen Space,而運行時常量池是方法區的一部分。
2.4.5.Native MethodStack是供本地方法(非java)使用的棧。每一個線程持有一個Native Method Stack。
3.JVM的運行原理簡介
Java 程序被javac工具編譯爲.class字節碼文件以後,咱們執行java命令,該class文件便被JVM的ClassLoader加載,能夠看出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的生命週期也結束。
2、JVM的內存管理和垃圾回收
JVM中的內存管理主要是指JVM對於Heap的管理,這是由於Stack,PCRegister和Native Method Stack都是和線程同樣的生命週期,在線程結束時天然能夠被再次使用。雖說,Stack的管理不是重點,可是也不是徹底不講究的。
1.棧的管理
JVM容許棧的大小是固定的或者是動態變化的。在Oracle的關於參數設置的官方文檔中有關於Stack的設置,是經過-Xss來設置其大小。關於Stack的默認大小對於不一樣機器有不一樣的大小,而且不一樣廠商或者版本號的jvm的實現其大小也不一樣,以下表是HotSpot的默認大小:
咱們通常經過減小常量,參數的個數來減小棧的增加,在程序設計時,咱們把一些常量定義到一個對象中,而後來引用他們能夠體現這一點。另外,少用遞歸調用也能夠減小棧的佔用由於棧幀中會存儲父棧幀,遞歸會致使父棧幀也在存或狀態,因此若是遞歸調用過深就會致使棧內存被大量佔用,甚至出現StackOverFlow。棧是不須要垃圾回收的,儘管說垃圾回收是java內存管理的一個很熱的話題,棧中的對象若是用垃圾回收的觀點來看,他永遠是live狀態,是能夠reachable的,因此也不須要回收,他佔有的空間隨着Thread的結束而釋放。
關於棧通常會發生如下兩種異常:
1.當線程中的計算所須要的棧超過所容許大小時,會拋出StackOverflowError。
2.當Java棧試圖擴展時,沒有足夠的存儲器來實現擴展,JVM會報OutOfMemoryError。
另外棧上有一點得注意的是,對於本地代碼調用,可能會在棧中申請內存,好比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區總有一個是空的。同時,根據程序須要,jvm提供對Survivor區複製次數的配置(-XX:MaxTenuringThreshold參數),即通過多少次複製後仍然存活的對象會被放到老年區,經過增多兩個Survivor區複製的次數能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能。
Old Space中則存放生命週期比較長的對象,並且有些比較大的新生對象也放在Old Space中,經過-XX:PretenureSizeThreshold設置,超過此大小的新生對象會直接放入老年區。
堆的大小經過-Xms和-Xmx來指定最小值和最大值,經過-Xmn來指定Young Generation的大小(一些老版本也用-XX:NewSize指定), 即上圖中的Eden加FromSpace和ToSpace的總大小。而後經過-XX:NewRatio來指定Eden區的大小,在Xms和Xmx相等的狀況下,該參數不須要設置。經過-XX:SurvivorRatio來設置Eden和一個Survivor區的比值。
堆異常分爲兩種,一種是Out ofMemory(OOM),一種是Memory Leak(ML)。MemoryLeak最終將致使OOM。實際應用中表現爲:從Console看,內存監控曲線一直在頂部,程序響應慢,從線程看,大部分的線程在進行GC,佔用比較多的CPU,最終程序異常終止,報OOM。OOM發生的時間不定,有短的一個小時,有長的10天一個月的。關於異常的處理,肯定OOM/ML異常後,必定要注意保護現場,能夠dump heap,若是沒有現場則開啓GCFlag收集垃圾回收日誌,而後進行分析,肯定問題所在。若是問題不是ML的話,通常經過增長Heap,增長物理內存來解決問題,是的話,就修改程序邏輯。
在此我向你們推薦一個架構學習交流羣。交流學習羣號:575745314 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多
3.垃圾回收
JVM中會在如下狀況觸發回收:對象沒有被引用,做用域發生未捕捉異常,程序正常執行完畢,程序執行了System.exit(),程序發生意外終止。
JVM中標記垃圾使用的算法是一種根搜索算法。簡單的說,就是從一個叫GC Roots的對象開始,向下搜索,若是一個對象不能達到GC Roots對象的時候,說明它能夠被回收了。這種算法比一種叫作引用計數法的垃圾標記算法要好,由於它避免了當兩個對象啊互相引用時沒法被回收的現象。
JVM中對於被標記爲垃圾的對象進行回收時又分爲了一下3種算法:
1.標記清除算法,該算法是從根集合掃描整個空間,標記存活的對象,而後在掃描整個空間對沒有被標記的對象進行回收,這種算法在存活對象較多時比較高效,但會產生內存碎片。
2.複製算法,該算法是從根集合掃描,並將存活的對象複製到新的空間,這種算法在存活對象少時比較高效。
3.標記整理算法,標記整理算法和標記清除算法同樣都會掃描並標記存活對象,在回收未標記對象的同時會整理被標記的對象,解決了內存碎片的問題。
JVM中,不一樣的 內存區域做用和性質不同,使用的垃圾回收算法也不同,因此JVM中又定義了幾種不一樣的垃圾回收器(圖中連線表明兩個回收器能夠同時使用):
1.Serial GC。從名字上看,串行GC意味着是一種單線程的,因此它要求收集的時候全部的線程暫停。這對於高性能的應用是不合理的,因此串行GC通常用於Client模式的JVM中。
2.ParNew GC。是在SerialGC的基礎上,增長了多線程機制。可是若是機器是單CPU的,這種收集器是比SerialGC效率低的。
3.Parrallel ScavengeGC。這種收集器又叫吞吐量優先收集器,而吞吐量=程序運行時間/(JVM執行回收的時間+程序運行時間),假設程序運行了100分鐘,JVM的垃圾回收佔用1分鐘,那麼吞吐量就是99%。ParallelScavenge 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放不下、對象只能放入舊生代,而此時舊生代也放不下形成的;concurrentmode failure是在執行CMS GC的過程當中同時有對象要放入舊生代,而此時舊生代空間不足形成的,由於CMS是併發執行的,執行GC的過程當中可能也會有對象被放入舊生代。
4.判斷MinorGC後,要晉升到TenuredSpace的對象大小大於TenuredSpace的大小,也會觸發FullGC。
能夠看出,當FullGC頻繁發生時,必定是內存出問題了。
3、JVM的數據格式規範和Class文件
1.數據類型規範
依據馮諾依曼的計算機理論,計算機最後處理的都是二進制的數,而JVM是怎麼把java文件最後轉化成了各個平臺均可以識別的二進制呢?JVM本身定義了一個抽象的存儲數據單位,叫作Word。一個字足夠大以持有byte、char、short、int、float、reference或者returnAdress的一個值,兩個字則足夠持有更大的類型long、double。它一般是主機平臺一個指針的大小,如32位的平臺上,字是32位。
同時JVM中定義了它所支持的基本數據類型,包括兩部分:數值類型和returnAddress類型。數值類型分爲整形和浮點型。
整形:
returnAddress類型的值是Java虛擬機指令的操做碼的指針。
對比java的基本數據類型,jvm的規範中沒有boolean類型。這是由於jvm中堆boolean的操做是經過int類型來進行處理的,而boolean數組則是經過byte數組來進行處理。
至於String,咱們知道它存儲在常量池中,但他不是基本數據類型,之因此能夠存在常量池中,是由於這是JVM的一種規定。若是查看String源碼,咱們就會發現,String其實就是一個基於基本數據類型char的數組。
2.字節碼文件
經過字節碼文件的格式咱們能夠看出jvm是如何規範數據類型的。下面是ClassFile的結構:
其中u一、u二、u4分別表明一、二、4個字節無符號數。
magic:
魔數,魔數的惟一做用是肯定這個文件是否爲一個能被虛擬機所接受的Class文件。魔數值固定爲0xCAFEBABE,不會改變。
minor_version、major_version:
分別爲Class文件的副版本和主版本。它們共同構成了Class文件的格式版本號。不一樣版本的虛擬機實現支持的Class文件版本號也相應不一樣,高版本號的虛擬機能夠支持低版本的Class文件,反之則不成立。
constant_pool_count:
常量池計數器,constant_pool_count的值等於constant_pool表中的成員數加1。
constant_pool[]:
常量池,constant_pool是一種表結構,它包含Class文件結構及其子結構中引用的全部字符串常量、類或接口名、字段名和其它常量。常量池不一樣於其餘,索引從1開始到constant_pool_count -1。
access_flags:
訪問標誌,access_flags是一種掩碼標誌,用於表示某個類或者接口的訪問權限及基礎屬性。access_flags的取值範圍和相應含義見下表:
this_class:
類索引,this_class的值必須是對constant_pool表中項目的一個有效索引值。constant_pool表在這個索引處的項必須爲CONSTANT_Class_info類型常量,表示這個Class文件所定義的類或接口。
super_class:
父類索引,對於類來講,super_class的值必須爲0或者是對constant_pool表中項目的一個有效索引值。若是它的值不爲0,那constant_pool表在這個索引處的項必須爲CONSTANT_Class_info類型常量,表示這個Class文件所定義的類的直接父類。固然,若是某個類super_class的值是0,那麼它一定是java.lang.Object類,由於只有它是沒有父類的。
interfaces_count:
接口計數器,interfaces_count的值表示當前類或接口的直接父接口數量。
interfaces[]:
接口表,interfaces[]數組中的每一個成員的值必須是一個對constant_pool表中項目的一個有效索引值,它的長度爲interfaces_count。每一個成員interfaces[i] 必須爲CONSTANT_Class_info類型常量。
fields_count:
字段計數器,fields_count的值表示當前Class文件fields[]數組的成員個數。
fields[]:
字段表,fields[]數組中的每一個成員都必須是一個fields_info結構的數據項,用於表示當前類或接口中某個字段的完整描述。
methods_count:
方法計數器,methods_count的值表示當前Class文件methods[]數組的成員個數。
methods[]:
方法表,methods[]數組中的每一個成員都必須是一個method_info結構的數據項,用於表示當前類或接口中某個方法的完整描述。
attributes_count:
屬性計數器,attributes_count的值表示當前Class文件attributes表的成員個數。
attributes[]:
屬性表,attributes表的每一個項的值必須是attribute_info結構。
3.jvm指令集
在Java虛擬機的指令集中,大多數的指令都包含了其操做所對應的數據類型信息。舉個例子,iload指令用於從局部變量表中加載int型的數據到操做數棧中,而fload指令加載的則是float類型的數據。這兩條指令的操做可能會是由同一段代碼來實現的,但它們必須擁有各自獨立的操做符。
對於大部分爲與數據類型相關的字節碼指令,他們的操做碼助記符中都有特殊的字符來代表專門爲哪一種數據類型服務:i表明對int類型的數據操做,l表明long,s表明short,b表明byte,c表明char,f表明float,d表明double,a表明reference。也有一些指令的助記符中沒有明確的指明操做類型的字母,例如arraylength指令,它沒有表明數據類型的特殊字符,但操做數永遠只能是一個數組類型的對象。還有另一些指令,例如無條件跳轉指令goto則是與數據類型無關的。
因爲Java虛擬機的操做碼長度只有一個字節,因此包含了數據類型的操做碼對指令集的設計帶來了很大的壓力(只有256個指令):若是每一種與數據類型相關的指令都支持Java虛擬機全部運行時數據類型的話,那恐怕就會超出一個字節所能表示的數量範圍了。所以,Java虛擬機的指令集對於特定的操做只提供了有限的類型相關指令去支持它,換句話說,指令集將會故意被設計成非徹底獨立的(Not Orthogonal,即並不是每種數據類型和每一種操做都有對應的指令)。有一些單獨的指令能夠在必要的時候用來將一些不支持的類型轉換爲可被支持的類型。
經過查閱jvm指令集和其對應的數據類型的關係發現,大部分的指令都沒有支持整數類型byte、char和short,甚至沒有任何指令支持boolean類型。編譯器會在編譯期或運行期會將byte和short類型的數據帶符號擴展(Sign-Extend)爲相應的int類型數據,將boolean和char類型數據零位擴展(Zero-Extend)爲相應的int類型數據。與之相似的,在處理boolean、byte、short和char類型的數組時,也會轉換爲使用對應的int類型的字節碼指令來處理。所以,大多數對於boolean、byte、short和char類型數據的操做,實際上都是使用相應的對int類型做爲運算類型(Computational Type)。
4、一個java類的實例分析
爲了瞭解JVM的數據類型規範和內存分配的大致狀況,下面舉個簡單的例子來講明一下ClassFile的結構:
經過javap工具咱們能看到這個簡單的類的結構,以下:
咱們能夠看到一些信息包括主副版本號、常量池、ACC_FLAGS等,再來打開Class文件看一下:
根據前面所述的ClassFile結構,咱們來分析下:
能夠看到前4個字節爲魔數,也就是0xCAFEBABE,這裏都是十六進制。
魔數後2個字節爲副版本號,這裏副版本號是0.
再後2個字節是主版本號0x0033,轉爲十進制,主版本號是51,和Javap工具所看到的同樣,這裏我用的JDK版本是1.7。
這兩個字節是常量池計數器,常量池的數量爲0x0017,轉爲十進制是23,也就是說常量池的索引爲1~22,這與Javap所看到的也相符。
常量池計數器後面就是常量池的內容,咱們根據javap所看到的信息找到最後一個常量池項java/lang/Object,在字節碼中找到對應的地方:
常量池後面兩個字節是訪問標誌access_flags:
值爲0x0021,在javap中咱們看到這個類的標誌是
其中ACC_PUBLIC的值爲0x0001,ACC_SUPER的值爲0x0020,與字節碼是相匹配的。
至於ClassFile的其餘結構,包括this_class、super_class、接口計數器、接口等等均可以經過一樣的方法進行分析,這裏就再也不多說了。
在此我向你們推薦一個架構學習交流羣。交流學習羣號:575745314 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多
5、關於jvm優化
無論是YGC仍是Full GC,GC過程當中都會對致使程序運行中中斷,正確的選擇不一樣的GC策略,調整JVM、GC的參數,能夠極大的減小因爲GC工做,而致使的程序運行中斷方面的問題,進而適當的提升Java程序的工做效率。可是調整GC是以個極爲複雜的過程,因爲各個程序具有不一樣的特色,如:web和GUI程序就有很大區別(Web能夠適當的停頓,但GUI停頓是客戶沒法接受的),並且因爲跑在各個機器上的配置不一樣(主要cup個數,內存不一樣),因此使用的GC種類也會不一樣。
1. gc策略
如今比較經常使用的是分代收集(generational collection,也是SUN VM使用的,J2SE1.2以後引入),即將內存分爲幾個區域,將不一樣生命週期的對象放在不一樣區域裏:younggeneration,tenured generation和permanet generation。絕大部分的objec被分配在young generation(生命週期短),而且大部分的object在這裏die。當younggeneration滿了以後,將引起minor collection(YGC)。在minor collection後存活的object會被移動到tenured generation(生命週期比較長)。最後,tenured generation滿以後觸發major collection。major collection(Full gc)會觸發整個heap的回收,包括回收young generation。permanet generation區域比較穩定,主要存放classloader信息。
young generation有eden、2個survivor 區域組成。其中一個survivor區域一直是空的,是eden區域和另外一個survivor區域在下一次copy collection後活着的objecy的目的地。object在survivo區域被複制直到轉移到tenured區。
咱們要儘可能減小 Full gc 的次數(tenuredgeneration通常比較大,收集的時間較長,頻繁的Full gc會致使應用的性能收到嚴重的影響)。
JVM(採用分代回收的策略),用較高的頻率對年輕的對象(young generation)進行YGC,而對老對象(tenuredgeneration)較少(tenuredgeneration 滿了後才進行)進行Full GC。這樣就不須要每次GC都將內存中全部對象都檢查一遍。
GC不會在主程序運行期對PermGen Space進行清理,因此若是你的應用中有不少CLASS(特別是動態生成類,固然permgen space存放的內容不只限於類)的話,就極可能出現PermGen Space錯誤。
2. 內存申請過程
1.JVM會試圖爲相關Java對象在Eden中初始化一塊內存區域;
2.當Eden空間足夠時,內存申請結束。不然到下一步;
3.JVM試圖釋放在Eden中全部不活躍的對象(minor collection),釋放後若Eden空間4.仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區;
5.Survivor區被用來做爲Eden及old的中間交換區域,當OLD區空間足夠時,Survivor區的對象會被移到Old區,不然會被保留在Survivor區;
6.當old區空間不夠時,JVM會在old區進行major collection;
7.徹底垃圾收集後,若Survivor及old區仍然沒法存放從Eden複製過來的部分對象,致使JVM沒法在Eden區爲新對象建立內存區域,則出現"Out of memory錯誤";
3.性能考慮
對於GC的性能主要有2個方面的指標:吞吐量throughput(工做時間不算gc的時間佔總的時間比)和暫停pause(gc發生時app對外顯示的沒法響應)。
1.Total Heap
默認狀況下,vm會增長/減小heap大小以維持free space在整個vm中佔的比例,這個比例由MinHeapFreeRatio和MaxHeapFreeRatio指定。
通常而言,server端的app會有如下規則:
對vm分配儘量多的memory;
將Xms和Xmx設爲同樣的值。若是虛擬機啓動時設置使用的內存比較小,這個時候又須要初始化不少對象,虛擬機就必須重複地增長內存。
處理器核數增長,內存也跟着增大。
2.The Young Generation
另一個對於app流暢性運行影響的因素是younggeneration的大小。young generation越大,minor collection越少;可是在固定heap size狀況下,更大的young generation就意味着小的tenured generation,就意味着更多的major collection(major collection會引起minorcollection)。
NewRatio反映的是young和tenuredgeneration的大小比例。NewSize和MaxNewSize反映的是young generation大小的下限和上限,將這兩個值設爲同樣就固定了younggeneration的大小(同Xms和Xmx設爲同樣)。
若是但願,SurvivorRatio也能夠優化survivor的大小,不過這對於性能的影響不是很大。SurvivorRatio是eden和survior大小比例。
通常而言,server端的app會有如下規則:
首先決定能分配給vm的最大的heap size,而後設定最佳的young generation的大小;
若是heap size固定後,增長young generation的大小意味着減少tenured generation大小。讓tenured generation在任什麼時候候夠大,可以容納全部live的data(留10%-20%的空餘)。
4.經驗總結
1.年輕代大小選擇
響應時間優先的應用:儘量設大,直到接近系統的最低響應時間限制(根據實際狀況選擇).在此種狀況下,年輕代收集發生的頻率也是最小的.同時,減小到達年老代的對象.
吞吐量優先的應用:儘量的設置大,可能到達Gbit的程度.由於對響應時間沒有要求,垃圾收集能夠並行進行,通常適合8CPU以上的應用.
避免設置太小.當新生代設置太小時會致使:1.YGC次數更加頻繁 2.可能致使YGC對象直接進入舊生代,若是此時舊生代滿了,會觸發FGC.
2.年老代大小選擇
響應時間優先的應用:年老代使用併發收集器,因此其大小須要當心設置,通常要考慮併發會話率和會話持續時間等一些參數.若是堆設置小了,能夠會形成內存碎片,高回收頻率以及應用暫停而使用傳統的標記清除方式;若是堆大了,則須要較長的收集時間.最優化的方案,通常須要參考如下數據得到:
a.併發垃圾收集信息、持久代併發收集次數、傳統GC信息、花在年輕代和年老代回收上的時間比例。
b.吞吐量優先的應用:通常吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代.緣由是,這樣能夠儘量回收掉大部分短時間對象,減小中期的對象,而年老代盡存放長期存活對象.
3.較小堆引發的碎片問題
由於年老代的併發收集器使用標記,清除算法,因此不會對堆進行壓縮.當收集器回收時,他會把相鄰的空間進行合併,這樣能夠分配給較大的對象.可是,當堆空間較小時,運行一段時間之後,就會出現"碎片",若是併發收集器找不到足夠的空間,那麼併發收集器將會中止,而後使用傳統的標記,清除方式進行回收.若是出現"碎片",可能須要進行以下配置:
-XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啓對年老代的壓縮.
-XX:CMSFullGCsBeforeCompaction=0:上面配置開啓的狀況下,這裏設置多少次Full GC後,對年老代進行壓縮
4.用64位操做系統,Linux下64位的jdk比32位jdk要慢一些,可是吃得內存更多,吞吐量更大
5.XMX和XMS設置同樣大,MaxPermSize和MinPermSize設置同樣大,這樣能夠減輕伸縮堆大小帶來的壓力
6.使用CMS的好處是用盡可能少的新生代,經驗值是128M-256M, 而後老生代利用CMS並行收集, 這樣能保證系統低延遲的吞吐效率。實際上cms的收集停頓時間很是的短,2G的內存, 大約20-80ms的應用程序停頓時間
7.系統停頓的時候多是GC的問題也多是程序的問題,多用jmap和jstack查看,或者killall -3 java,而後查看java控制檯日誌,能看出不少問題。(相關工具的使用方法將在後面的blog中介紹)
8.仔細瞭解本身的應用,若是用了緩存,那麼年老代應該大一些,緩存的HashMap不該該無限制長,建議採用LRU算法的Map作緩存,LRUMap的最大長度也要根據實際狀況設定。
9.採用併發回收時,年輕代小一點,年老代要大,由於年老大用的是併發回收,即便時間長點也不會影響其餘程序繼續運行,網站不會停頓
10.-Xnoclassgc禁用類垃圾回收,性能會高一點;
11.-XX:+DisableExplicitGC禁止System.gc(),省得程序員誤調用gc方法影響性能
12.JVM參數的設置(特別是 –Xmx –Xms –Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold等參數的設置沒有一個固定的公式,須要根據PV old區實際數據 YGC次數等多方面來衡量。爲了不promotion faild可能會致使xmn設置偏小,也意味着YGC的次數會增多,處理併發訪問的能力降低等問題。每一個參數的調整都須要通過詳細的性能測試,才能找到特定應用的最佳配置。
5.promotion failed:
垃圾回收時promotion failed是個很頭痛的問題,通常多是兩種緣由產生,第一個緣由是救助空間不夠,救助空間裏的對象還不該該被移動到年老代,但年輕代又有不少對象須要放入救助空間;第二個緣由是年老代沒有足夠的空間接納來自年輕代的對象;這兩種狀況都會轉向Full GC,網站停頓時間較長。
解決方方案一:
第一個緣由個人最終解決辦法是去掉救助空間,設置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0便可,第二個緣由個人解決辦法是設置CMSInitiatingOccupancyFraction爲某個值(假設70),這樣年老代空間到70%時就開始執行CMS,年老代有足夠的空間接納來自年輕代的對象。
解決方案一的改進方案:
又有改進了,上面方法不太好,由於沒有用到救助空間,因此年老代容易滿,CMS執行會比較頻繁。我改善了一下,仍是用救助空間,可是把救助空間加大,這樣也不會有promotionfailed。具體操做上,32位Linux和64位Linux好像不同,64位系統彷佛只要配置MaxTenuringThreshold參數,CMS仍是有暫停。爲了解決暫停問題和promotion failed問題,最後我設置-XX:SurvivorRatio=1 ,並把MaxTenuringThreshold去掉,這樣即沒有暫停又不會有promotoinfailed,並且更重要的是,年老代和永久代上升很是慢(由於好多對象到不了年老代就被回收了),因此CMS執行頻率很是低,好幾個小時才執行一次,這樣,服務器都不用重啓了。
-Xmx4000M-Xms4000M -Xmn600M -XX:PermSize=500M -XX:MaxPermSize=500M -Xss256K-XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC-XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0-XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M-XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly-XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0-XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps-XX:+PrintHeapAtGC -Xloggc:log/gc.log
6.CMSInitiatingOccupancyFraction值與Xmn的關係公式
上面介紹了promontion faild產生的緣由是EDEN空間不足的狀況下將EDEN與From survivor中的存活對象存入To survivor區時,To survivor區的空間不足,再次晉升到old gen區,而old gen區內存也不夠的狀況下產生了promontion faild從而致使full gc.那能夠推斷出:eden+from survivor < old gen區剩餘內存時,不會出現promontionfaild的狀況,即:
(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2)) 進而推斷出:
CMSInitiatingOccupancyFraction<=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100
例如:
當xmx=128 xmn=36 SurvivorRatior=1時CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100=73.913
當xmx=128 xmn=24 SurvivorRatior=1時CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615…
當xmx=3000 xmn=600 SurvivorRatior=1時 CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33
CMSInitiatingOccupancyFraction低於70% 須要調整xmn或SurvivorRatior值。
對此,網上牛人們得出的公式是是:(Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100>=Xmn。
jvm的調優
1、JVM內存模型及垃圾收集算法
1.根據Java虛擬機規範,JVM將內存劃分爲:
New(年輕代)
Tenured(年老代)
永久代(Perm)
其中New和Tenured屬於堆內存,堆內存會從JVM啓動參數(-Xmx:3G)指定的內存中分配,Perm不屬於堆內存,有虛擬機直接分配,但能夠經過-XX:PermSize -XX:MaxPermSize等參數調整其大小。
年輕代(New):年輕代用來存放JVM剛分配的Java對象
年老代(Tenured):年輕代中通過垃圾回收沒有回收掉的對象將被Copy到年老代
永久代(Perm):永久代存放Class、Method元信息,其大小跟項目的規模、類、方法的量有關,通常設置爲128M就足夠,設置原則是預留30%的空間。
New又分爲幾個部分:
Eden:Eden用來存放JVM剛分配的對象
Survivor1
Survivro2:兩個Survivor空間同樣大,當Eden中的對象通過垃圾回收沒有被回收掉時,會在兩個Survivor之間來回Copy,當知足某個條件,好比Copy次數,就會被Copy到Tenured。顯然,Survivor只是增長了對象在年輕代中的逗留時間,增長了被垃圾回收的可能性。
2.垃圾回收算法
垃圾回收算法能夠分爲三類,都基於標記-清除(複製)算法:
Serial算法(單線程)
並行算法
併發算法
JVM會根據機器的硬件配置對每一個內存代選擇適合的回收算法,好比,若是機器多於1個核,會對年輕代選擇並行算法,關於選擇細節請參考JVM調優文檔。
稍微解釋下的是,並行算法是用多線程進行垃圾回收,回收期間會暫停程序的執行,而併發算法,也是多線程回收,但期間不中止應用執行。因此,併發算法適用於交互性高的一些程序。通過觀察,併發算法會減小年輕代的大小,其實就是使用了一個大的年老代,這反過來跟並行算法相比吞吐量相對較低。
還有一個問題是,垃圾回收動做什麼時候執行?
當年輕代內存滿時,會引起一次普通GC,該GC僅回收年輕代。須要強調的時,年輕代盡是指Eden代滿,Survivor滿不會引起GC
當年老代滿時會引起Full GC,Full GC將會同時回收年輕代、年老代
當永久代滿時也會引起Full GC,會致使Class、Method元信息的卸載
另外一個問題是,什麼時候會拋出OutOfMemoryException,並非內存被耗空的時候才拋出
JVM98%的時間都花費在內存回收
每次回收的內存小於2%
知足這兩個條件將觸發OutOfMemoryException,這將會留給系統一個微小的間隙以作一些Down以前的操做,好比手動打印Heap Dump。
2、內存泄漏及解決方法
1.系統崩潰前的一些現象:
每次垃圾回收的時間愈來愈長,由以前的10ms延長到50ms左右,FullGC的時間也有以前的0.5s延長到四、5s
FullGC的次數愈來愈多,最頻繁時隔不到1分鐘就進行一次FullGC
年老代的內存愈來愈大而且每次FullGC後年老代沒有內存被釋放
以後系統會沒法響應新的請求,逐漸到達OutOfMemoryError的臨界值。
2.生成堆的dump文件
經過JMX的MBean生成當前的Heap信息,大小爲一個3G(整個堆的大小)的hprof文件,若是沒有啓動JMX能夠經過Java的jmap命令來生成該文件。
3.分析dump文件
下面要考慮的是如何打開這個3G的堆信息文件,顯然通常的Window系統沒有這麼大的內存,必須藉助高配置的Linux。固然咱們能夠藉助X-Window把Linux上的圖形導入到Window。咱們考慮用下面幾種工具打開該文件:
Visual VM
IBM HeapAnalyzer
JDK 自帶的Hprof工具
使用這些工具時爲了確保加載速度,建議設置最大內存爲6G。使用後發現,這些工具都沒法直觀地觀察到內存泄漏,Visual VM雖能觀察到對象大小,但看不到調用堆棧;HeapAnalyzer雖然能看到調用堆棧,卻沒法正確打開一個3G的文件。所以,咱們又選用了Eclipse專門的靜態內存分析工具:Mat。
4.分析內存泄漏
經過Mat咱們能清楚地看到,哪些對象被懷疑爲內存泄漏,哪些對象佔的空間最大及對象的調用關係。針對本案,在ThreadLocal中有不少的JbpmContext實例,通過調查是JBPM的Context沒有關閉所致。
另,經過Mat或JMX咱們還能夠分析線程狀態,能夠觀察到線程被阻塞在哪一個對象上,從而判斷系統的瓶頸。
5.迴歸問題
Q:爲何崩潰前垃圾回收的時間愈來愈長?
A:根據內存模型和垃圾回收算法,垃圾回收分兩部分:內存標記、清除(複製),標記部分只要內存大小固定時間是不變的,變的是複製部分,由於每次垃圾回收都有一些回收不掉的內存,因此增長了複製量,致使時間延長。因此,垃圾回收的時間也能夠做爲判斷內存泄漏的依據
Q:爲何Full GC的次數愈來愈多?
A:所以內存的積累,逐漸耗盡了年老代的內存,致使新對象分配沒有更多的空間,從而致使頻繁的垃圾回收
Q:爲何年老代佔用的內存愈來愈大?
A:由於年輕代的內存沒法被回收,愈來愈多地被Copy到年老代
3、性能調優
除了上述內存泄漏外,咱們還發現CPU長期不足3%,系統吞吐量不夠,針對8core×16G、64bit的Linux服務器來講,是嚴重的資源浪費。
在CPU負載不足的同時,偶爾會有用戶反映請求的時間過長,咱們意識到必須對程序及JVM進行調優。從如下幾個方面進行:
線程池:解決用戶響應時間長的問題
鏈接池
JVM啓動參數:調整各代的內存比例和垃圾回收算法,提升吞吐量
程序算法:改進程序邏輯算法提升性能
1.Java線程池(java.util.concurrent.ThreadPoolExecutor)
大多數JVM6上的應用採用的線程池都是JDK自帶的線程池,之因此把成熟的Java線程池進行羅嗦說明,是由於該線程池的行爲與咱們想象的有點出入。Java線程池有幾個重要的配置參數:
corePoolSize:核心線程數(最新線程數)
maximumPoolSize:最大線程數,超過這個數量的任務會被拒絕,用戶能夠經過RejectedExecutionHandler接口自定義處理方式
keepAliveTime:線程保持活動的時間
workQueue:工做隊列,存放執行的任務
Java線程池須要傳入一個Queue參數(workQueue)用來存放執行的任務,而對Queue的不一樣選擇,線程池有徹底不一樣的行爲:
SynchronousQueue:一個無容量的等待隊列,一個線程的insert操做必須等待另外一線程的remove操做,採用這個Queue線程池將會爲每一個任務分配一個新線程
LinkedBlockingQueue :無界隊列,採用該Queue,線程池將忽略maximumPoolSize參數,僅用corePoolSize的線程處理全部的任務,未處理的任務便在LinkedBlockingQueue中排隊
ArrayBlockingQueue: 有界隊列,在有界隊列和maximumPoolSize的做用下,程序將很難被調優:更大的Queue和小的maximumPoolSize將致使CPU的低負載;小的Queue和大的池,Queue就沒起動應有的做用。
其實咱們的要求很簡單,但願線程池能跟鏈接池同樣,能設置最小線程數、最大線程數,當最小數<任務<最大數時,應該分配新的線程處理;當任務>最大數時,應該等待有空閒線程再處理該任務。
但線程池的設計思路是,任務應該放到Queue中,當Queue放不下時再考慮用新線程處理,若是Queue滿且沒法派生新線程,就拒絕該任務。設計致使「先放等執行」、「放不下再執行」、「拒毫不等待」。因此,根據不一樣的Queue參數,要提升吞吐量不能一味地增大maximumPoolSize。
固然,要達到咱們的目標,必須對線程池進行必定的封裝,幸運的是ThreadPoolExecutor中留了足夠的自定義接口以幫助咱們達到目標。咱們封裝的方式是:
以SynchronousQueue做爲參數,使maximumPoolSize發揮做用,以防止線程被無限制的分配,同時能夠經過提升maximumPoolSize來提升系統吞吐量
自定義一個RejectedExecutionHandler,當線程數超過maximumPoolSize時進行處理,處理方式爲隔一段時間檢查線程池是否能夠執行新Task,若是能夠把拒絕的Task從新放入到線程池,檢查的時間依賴keepAliveTime的大小。
2.鏈接池(org.apache.commons.dbcp.BasicDataSource)
在使用org.apache.commons.dbcp.BasicDataSource的時候,由於以前採用了默認配置,因此當訪問量大時,經過JMX觀察到不少Tomcat線程都阻塞在BasicDataSource使用的Apache ObjectPool的鎖上,直接緣由當時是由於BasicDataSource鏈接池的最大鏈接數設置的過小,默認的BasicDataSource配置,僅使用8個最大鏈接。
我還觀察到一個問題,當較長的時間不訪問系統,好比2天,DB上的Mysql會斷掉因此的鏈接,致使鏈接池中緩存的鏈接不能用。爲了解決這些問題,咱們充分研究了BasicDataSource,發現了一些優化的點:
Mysql默認支持100個連接,因此每一個鏈接池的配置要根據集羣中的機器數進行,若有2臺服務器,可每一個設置爲60
initialSize:參數是一直打開的鏈接數
minEvictableIdleTimeMillis:該參數設置每一個鏈接的空閒時間,超過這個時間鏈接將被關閉
timeBetweenEvictionRunsMillis:後臺線程的運行週期,用來檢測過時鏈接
maxActive:最大能分配的鏈接數
maxIdle:最大空閒數,當鏈接使用完畢後發現鏈接數大於maxIdle,鏈接將被直接關閉。只有initialSize < x < maxIdle的鏈接將被按期檢測是否超期。這個參數主要用來在峯值訪問時提升吞吐量。
initialSize是如何保持的?通過研究代碼發現,BasicDataSource會關閉全部超期的鏈接,而後再打開initialSize數量的鏈接,這個特性與minEvictableIdleTimeMillis、timeBetweenEvictionRunsMillis一塊兒保證了全部超期的initialSize鏈接都會被從新鏈接,從而避免了Mysql長時間無動做會斷掉鏈接的問題。
3.JVM參數
在JVM啓動參數中,能夠設置跟內存、垃圾回收相關的一些參數設置,默認狀況不作任何設置JVM會工做的很好,但對一些配置很好的Server和具體的應用必須仔細調優才能得到最佳性能。經過設置咱們但願達到一些目標:
GC的時間足夠的小
GC的次數足夠的少
發生Full GC的週期足夠的長
前兩個目前是相悖的,要想GC時間小必需要一個更小的堆,要保證GC次數足夠少,必須保證一個更大的堆,咱們只能取其平衡。
(1)針對JVM堆的設置通常,能夠經過-Xms -Xmx限定其最小、最大值,爲了防止垃圾收集器在最小、最大之間收縮堆而產生額外的時間,咱們一般把最大、最小設置爲相同的值
(2)年輕代和年老代將根據默認的比例(1:2)分配堆內存,能夠經過調整兩者之間的比率NewRadio來調整兩者之間的大小,也能夠針對回收代,好比年輕代,經過 -XX:newSize -XX:MaxNewSize來設置其絕對大小。一樣,爲了防止年輕代的堆收縮,咱們一般會把-XX:newSize -XX:MaxNewSize設置爲一樣大小
(3)年輕代和年老代設置多大才算合理?這個我問題毫無疑問是沒有答案的,不然也就不會有調優。咱們觀察一下兩者大小變化有哪些影響
更大的年輕代必然致使更小的年老代,大的年輕代會延長普通GC的週期,但會增長每次GC的時間;小的年老代會致使更頻繁的Full GC
更小的年輕代必然致使更大年老代,小的年輕代會致使普通GC很頻繁,但每次的GC時間會更短;大的年老代會減小Full GC的頻率
如何選擇應該依賴應用程序對象生命週期的分佈狀況:若是應用存在大量的臨時對象,應該選擇更大的年輕代;若是存在相對較多的持久對象,年老代應該適當增大。但不少應用都沒有這樣明顯的特性,在抉擇時應該根據如下兩點:(A)本着Full GC儘可能少的原則,讓年老代儘可能緩存經常使用對象,JVM的默認比例1:2也是這個道理 (B)經過觀察應用一段時間,看其餘在峯值時年老代會佔多少內存,在不影響Full GC的前提下,根據實際狀況加大年輕代,好比能夠把比例控制在1:1。但應該給年老代至少預留1/3的增加空間
(4)在配置較好的機器上(好比多核、大內存),能夠爲年老代選擇並行收集算法:-XX:+UseParallelOldGC,默認爲Serial收集
(5)線程堆棧的設置:每一個線程默認會開啓1M的堆棧,用於存放棧幀、調用參數、局部變量等,對大多數應用而言這個默認值太了,通常256K就足用。理論上,在內存不變的狀況下,減小每一個線程的堆棧,能夠產生更多的線程,但這實際上還受限於操做系統。
(4)能夠經過下面的參數打Heap Dump信息
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
經過下面參數能夠控制OutOfMemoryError時打印堆的信息
-XX:+HeapDumpOnOutOfMemoryError
請看一下一個時間的Java參數配置:(服務器:Linux 64Bit,8Core×16G)
JAVA_OPTS="$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G"
通過觀察該配置很是穩定,每次普通GC的時間在10ms左右,Full GC基本不發生,或隔很長很長的時間才發生一次
jvm-垃圾回收
一:爲何須要垃圾回收?
jvm把內存管理權從開發人員收回,開發人員只須要建立數據對象便可,內存的分配和回收都由jvm自動完成。
程序只管建立對象,無論對象的回收,內存最終會被耗盡。
二:怎麼判斷對象爲垃圾?
若是要實現垃圾回收,首先必須能判斷哪些對象是垃圾。
對象再也不被使用就認爲是垃圾。jvm自動回收垃圾,但它如何才能知道一個對象是否再也不被使用?
常見的策略有以下兩種:引用計數器 、可達性檢測。
2.1 引用計數器:
即若是一個對象被外部引用則計數器加 1, 反之減 1。若是計數器爲0,則說明當前對外象沒有被任何外部使用,則認爲是垃圾。
優勢:實現簡單
缺點:沒法解決循環引用的問題;如:對象A,B相互引用,除此再沒有被其它對象引用,那麼它們兩個都是垃圾,但計數器卻均爲1,而沒法回收。
注意事項:引用計數器只是一個理論方案,歷來沒有一個主流的jvm使用這種方式
2.2 可達性檢測
引用計數器沒法解決循環引用的問題,所以更好的辦法是經過可達性分析。jvm中的任何非垃圾對象經過引用鏈向上追溯,均可以到達一些根對象(法方區的靜態變量、常量、棧中的變量),這些根對象都是存活的對象,那麼被活對象引用的對象頗有可能會繼續使用,所以反過來,從根對象向下追溯到的對象均可以認爲是存活的對象。這種從根對象追溯的方法稱爲可達性分析。
以下:從根對象向下追溯,紅色標記的對象是不可達的,所以它們就是垃圾,會被GC回收。
2.3 根對象種類
能夠作爲GC root(根對象)的對象有如下幾種:
虛擬機棧(棧幀中變量引用的對象)
方法區中靜態屬性(static 屬性)
方法區中的常量(static final),(jdk8及以上,爲元數據區)
本地方法棧中引用的對象
三:垃圾回收算法
標記出哪些對象是垃圾後,就須要對這些垃圾對象進行回收。
經常使用的回收算法有:標記-清除、複製、標記-整理
3.1 標記-清除
經過標記、清除兩個階段回收垃圾對象。由於標記的是存活對象,清除的是非存活對象,因此須要兩個階段:先標記,再遍歷全部對象,過濾出非存活對象。
以下圖:(綠色-存活對象;紅色-垃圾;白色-空閒)
首先,經過可達性分析,標記出存活的對象(綠色塊)
其次,遍歷堆中全部對象,把非存活的對象所有清空。
優勢:實現簡單,而且是其它算法的基礎
缺點:A:標記效率不高,清除算法也不高(遍歷全部對象進行清除).
B:產生大量內存碎片
3.2 複製算法
爲了解決標記-清除 算法的效率問題,使用複製算法。
複製算法須要一塊一樣大小額外的內存作爲中轉。
由於複製的是存活對象,不需再次遍歷。
步驟:經過可達性分析,標記出存活對象,並同時把存活對象複製到另外一塊對等內存。
當全部存活對象都複製完後,直接清空原內存塊(不須要遍歷,直接移動堆頂指針便可)。
優勢: 不須要兩階段,存活對象少時效率高。
沒有內存碎片
缺點:須要額片內存,同一時間總有一塊內存處於備用狀態-浪費內存。
存活對象不少時效率也不高(主要是由於對象複製代價高昂)
使用場景:存活對象多,內存緊張的場景。
複製算法變種:
複製算法最大的缺點是須要一個相同大小的內存塊,爲了減小內存浪費,複製算法還有一種變種。
若是對象中存活的不多,就不須要一個相同大小的額外內存塊,而只須要兩個小內存塊,交替作爲中轉站就能夠完美解決。
前提:存活的對象不多,IBM研究代表新生代90%以上甚至98%的對象朝生夕死。
步驟:
A:設置三塊內存,第一塊大內存塊,第二第三爲兩個相等的小內存塊
B:建立對象分配置在大內存塊和 兩小內存塊中的任一個,另一小內存塊保持空閒備用。
C:回收:經過可達性分析,標記出第一塊和其中使用的小塊內存中存活對象,同時把存活對象複製到備用的另外一塊小內存中
D:清空大內存塊和被回收的小塊內存。此時:大內存被清空,其中兩塊小內存:一塊清空,一塊保存了上次存活的數
E:而後交替使用兩塊小內存塊作爲清空大內存和另外一塊小內存的中轉。
優勢:減小了內存浪費,同時又保持了複製算法的優勢。
缺點:未徹底杜絕內存浪費,同時大數據量時,效率低;存活對象數量佔比較大時,小內存塊沒法作爲中轉站。
使用場景:在存活對象較少,追求高效率,內存無碎片的場景。
3.3 標記-整理
標記清除算法效率低,碎片嚴重; 複製算法存活對象少時效率高,無碎片,但內存浪費;爲了折中兩種算法的優勢,有人提供另外一種算法:標記-整理算法。
步驟:
A:根據可達性分析,標記出全部存活的對象
B:遍歷全部對象,過濾出非存活的對象,並把這些對象一個一個,從內存的某一個角落順序排列。
優勢:沒有內存浪費,無碎片
缺點:效率最低,小於標記清除(須要兩個階段<標記,移動>;移動相似複製,代價高於直接清除,存活對象越多,移動代價越大)
四:分代算法
準確的講,分代算法不是一種回收算法,它只是按對象生命週期和特色不一樣,合理選用以上三種回收算法的手段。
內存模型中,咱們大概瞭解了堆內存的分代結構以下:
爲何須要分代?
由於不一樣的對象生命週期不一樣,有的很長(如:session),有的很短(如:方法中的變動);若是不分代,每次可達性分析標記時,都要遍歷暫時不會回收的老對象,當老對象愈來愈多時,重複對老對象的無用遍利檢查,會嚴重影響回收性能。
若是把對象按年齡隔離,分紅新生代和老年代,老年代保存生命週期長的對象,新生代保存新建立的對象,那麼老年代就能夠長時間不回收,而新年代大部分是朝生夕死,就能夠頻繁回收。即保證了效率,又保證了新生代內存的及時回收。
總結:新生代:時間換空間(頻繁回收:因爲存活的數據量少,頻繁回收的代價也能夠接受)
老年代:空間換時間(須要時回收:存活的多,頻繁回收嚴重影響性能;有些對象可能已經變垃圾了,但仍然存在老年代中,等到新生代不夠或其它條件時,纔回收老年代)
在此我向你們推薦一個架構學習交流羣。交流學習羣號:575745314 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多
如何區分新老對象?
這個與垃圾回收器實現有關,對應回收器有相關的配置。
主要有幾種狀況:
大對象直接進行老年代
年齡達到必定閥值(每經歷過一次回收還活着:年齡加1, 默認閥值爲:15,可配置)
survivor空間中相同年齡全部對象大小的總和超過survivor空間的一半時,即便沒達到年齡閥值
五:垃圾回收器
垃圾回收算法只是垃圾回收的理論基礎,垃圾回收器是對垃圾回算法的實現。
垃圾回收器關注三個方法:A:垃圾回收算法選擇 B:串行和並行(併發)的選擇 C:新老代的選擇
下面先了解一下jvm中的垃圾回收器種類:
垃圾回收器根據新老代不一樣區分,一部分只用於新生代回收(Serial、ParNew、Parallel),一部分只用於老年代(Serial old、CMS、Parallel old); G1是一個特殊的存在,後續再講。
下面我一個一個分析各自的原理及特色,而後分析他們爲何只能使用新生代或老年代;以及實戰中如何選擇。
5.1 Serial
serial/serial old 收集示意圖(圖片來自:JVM系列之垃圾回收(二))
使用於:新生代
垃圾回收算法:複製算法
串行/並行/併發:串行,單線程
stw:是
serial是一個單線程,且用於新生代的垃圾回收器。它運行時,須要stw,暫停全部用戶線程。因此,堆配置過大,且垃圾太多時,會致使程序有明顯的停頓。
因爲新生代是存活量少,回收頻繁,因此必須使用最高效的回收算法-複製算法;複製算法大量存活數據,且須要額外內存的狀況下是不符合老年代的,所以當前回收器只能用於新生代。
注意:此收集器,只適用於client模式,不適用於生產環境的server模式(當今服務器已經不多有單cpu,單線程在多cpu下,會浪費過多cpu資源,致使垃圾回收停頓時間過長和頻繁)
5.2 ParNew
(圖片來自:JVM系列之垃圾回收(二))
分代:用於新生代
垃圾回收算法:複製算法
串行/並行/併發:併發,多線程
stw:是
ParNew是serial收集器的多線程模式,除此以外沒有任何區別。多線程大大提升了多cpu服務器的垃圾回收效率,減小停頓時間。
5.3 Parallel Scavenge
分代:用於新生代
垃圾回收算法:複製算法
串行/並行/併發:並行,多線程
stw:是
Parallel Scavenge 與 ParNew同樣也是多線程,可是與ParNew不一樣的是,它關注的點是垃圾回收的吞吐量(用戶線程時間/(用戶線程時間 + 垃圾回收時間)),也就是:它指望儘量壓榨cpu,多用於業務捃,它關注的是總體,而不是一次。
如:假如每分鐘執行1000次垃圾回收,每次的停頓時間很短,但1000次總停頓時間要高於 每分種100次的時間。那麼100次垃圾回收就是Parallel Scavenge指望的。
5.4 Serial old
分代:用於老年代
垃圾回收算法:標記-整理算法
串行/並行/併發:串行,單線程
stw:是
因爲老年代,活的多,死的少,且最好沒有碎片:標記整理算法;
跟Serial收集器同樣,當前收集器也是單線程,所以也不適合多核時代的服務器上,是默認的client模式,同時作cms收集器失敗時的備選收集器(由於cms是併發的,若是併發失敗,就不要併發了,因此使用了serial Old)。
5.5 CMS
(圖片來自:JVM系列之垃圾回收(二))
分代:用於老年代
垃圾回收算法:標記-清除算法,有碎片
串行/並行/併發:多線程
stw:初始標記stw; 從新標記stw
CMS是首個併發收集器,垃圾回假步驟中的部分階段能夠與用戶線程併發執行。
垃圾回收器的最終目標就是:減小垃圾回收對用戶線程的影響(停頓頻率小、停頓時間少)。
爲此,CMS把垃圾回收分爲四個階段,把不須要停頓的階段與用戶線程一塊兒執行:
初始標記
併發標記
從新標記
併發清理
初始標記:從GC ROOTS只標記一級對象(存活的),因此速度很快;但須要stw。
併發標記:從一級對象開始向下追塑引用鏈,標記引用鏈上的對象;不須要stw,與用戶線程併發執行。速度是慢。
從新標記:修正併發標記過程當中,因用戶線程繼續進行而致使標記變動的那部分對象;速度比初始標記慢,但比並發標記快不少。(可是:究竟是修正了標記存活的對象仍是其它?若是是修改存活的,那麼能夠作爲浮動垃圾等到下一次回收便可阿???)
併發清理:垃圾回收線程與用戶線程併發執行,清除垃圾(若是標記的活着對象,那麼不stw若是清除垃圾,此時若是用戶線程又產生對象了?經過ooM?暫時沒想通)
優勢:單次停頓的時間更短
缺點:有碎片
5.6 Parallel old
(圖片來自:JVM系列之垃圾回收(二))
分代:用於老年代
垃圾回收算法:標記-整理算法
串行/並行/併發:多線程
stw:是