jvm原理
Java虛擬機是整個java平臺的基石,是java技術實現硬件無關和操作系統無關的關鍵環節,是java語言生成極小體積的編譯代碼的運行平臺,是保護用戶機器免受惡意代碼侵襲的保護屏障。JVM是虛擬機,也是一種規范,他遵循著馮·諾依曼體系結構的設計原理。馮·諾依曼體系結構中,指出計算機處理的數據和指令都是二進制數,采用存儲程序方式不加區分的存儲在同一個存儲器里,并且順序執行,指令由操作碼和地址碼組成,操作碼決定了操作類型和所操作的數的數字類型,地址碼則指出地址碼和操作數。從dos到window8,從unix到ubuntu和CentOS,還有MACOS等等,不同的操作系統指令集以及數據結構都有著差異,而JVM通過在操作系統上建立虛擬機,自己定義出來的一套統一的數據結構和操作指令,把同一套語言翻譯給各大主流的操作系統,實現了跨平臺運行,可以說JVM是java的核心,是java可以一次編譯到處運行的本質所在。
一、JVM的組成和運行原理
JVM的畢竟是個虛擬機,是一種規范,雖說符合馮諾依曼的計算機設計理念,但是他并不是實體計算機,所以他的組成也不是什么存儲器,控制器,運算器,輸入輸出設備。在我看來,JVM放在運行在真實的操作系統中表現的更像應用或者說是進程,他的組成可以理解為JVM這個進程有哪些功能模塊,而這些功能模塊的運作可以看做是JVM的運行原理。JVM有多種實現,例如Oracle的JVM,HP的JVM和IBM的JVM等,而在本文中研究學習的則是使用最廣泛的Oracle的HotSpot JVM。
?1.JVM在JDK中的位置。
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的兩種不同的實現,前者可以減少啟動時間和內存占用,而后者則提供更加優秀的程序運行速度。
??2.JVM的組成
JVM由4大部分組成:ClassLoader,Runtime Data Area,Execution Engine,Native Interface。
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的生命周期也結束。
二、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,增加物理內存來解決問題,是的話,就修改程序邏輯。
在此我向大家推薦一個架構學習交流群。交流學習群號:521353348??里面會分享一些資深架構師錄制的視頻錄像:有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頻繁發生時,一定是內存出問題了。
三、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的結構:
其中u1、u2、u4分別代表1、2、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)。
四、一個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、接口計數器、接口等等都可以通過同樣的方法進行分析,這里就不再多說了。
在此我向大家推薦一個架構學習交流群。交流學習群號:521353348??里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化、分布式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多
五、關于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的調優
一、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。
二、內存泄漏及解決方法
?1.系統崩潰前的一些現象:
每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右,FullGC的時間也有之前的0.5s延長到4、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到年老代
三、性能調優
?除了上述內存泄漏外,我們還發現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),有的很短(如:方法中的變更);如果不分代,每次可達性分析標記時,都要遍歷暫時不會回收的老對象,當老對象越來越多時,重復對老對象的無用遍利檢查,會嚴重影響回收性能。
如果把對象按年齡隔離,分成新生代和老年代,老年代保存生命周期長的對象,新生代保存新創建的對象,那么老年代就可以長時間不回收,而新年代大部分是朝生夕死,就可以頻繁回收。即保證了效率,又保證了新生代內存的及時回收。
總結:新生代:時間換空間(頻繁回收:由于存活的數據量少,頻繁回收的代價也可以接受)
? ? ? ? ?老年代:空間換時間(需要時回收:存活的多,頻繁回收嚴重影響性能;有些對象可能已經變垃圾了,但仍然存在老年代中,等到新生代不夠或其它條件時,才回收老年代)
在此我向大家推薦一個架構學習交流群。交流學習群號:521353348??里面會分享一些資深架構師錄制的視頻錄像:有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:是