Jvm 總結

Java 虛擬機主要分爲三個部分:類加載器、運行時數據區和執行引擎,其中類類加載器負責將類的字節碼文件加載到內存中,運行時數據區存儲jvm運行時產生的數據,執行引擎負責浮動程度的執行。

類加載器

類加載器就是我們經常說的ClassLoader,Java提供了三種類型ClassLoader,分別是BootstrapClassLoader(啓動類加載器)、ExtClassLoader(擴展類加載器)和AppClassLoader(應用程序類加載器),其中啓動類加載器負責加載java核心類庫,擴展類加載器負責加載lib/ext目錄下的類庫,應用程序類加載器負責加載類路徑下的類,這三個類加載器的關係是:

AppClassLoader extends ExtClassLoader extends BootstrapLoader

Java中的類加載採用雙親委派機制,對於加載請求首先會轉給父類加載器加載,只有父類加載器加載不到時,在逐級向下傳遞,雙親委派機制兩個好處:

  • 保證沙箱安全
  • 避免類被重複加載

類加載的過程包括加載、驗證、準備、解析和初始化,其中加載主要是將class字節碼文件以數據流的方式加載到內存,驗證階段會對加載進來的字節碼格式進行驗證,如果不符合jvm規範則會報錯,準備階段是爲靜態變量分配內存空間並賦予初始值,解析階段會將一些靜態的符號引用轉化爲直接引用,初始化階段對靜態變量按照代碼邏輯進行初始化賦值。

運行時數據區

運行時數據區是jvm中最核心的內存區域,存儲了所有的類元信息、對象實例等數據。運行時數據區分爲線程棧、本地方法棧、程序計數器、方法區和堆空間 五個部分。其中線程棧、本地方法棧和程序計數器是線程私有的區域,堆空間和方法區是公共的內存區域。

線程棧用來存儲當前線程方法調用的棧幀數據,棧幀是對方法調用的抽象,線程沒調用一個方法,就會產生一個棧幀並被壓入到線程棧中,方法執行結束後,棧幀從線程棧中彈出。棧幀中存儲了方法的臨時變量表、操作數棧、動態鏈接和方法的出口。

本地方法棧的作用和本地方法棧類似,知識它是作用於本地方法調用,而不是java方法的調用。

程序計數器用來記錄當前線程的執行位置,以便當發生cpu時間片輪轉時,線程能夠恢復到正確的位置,繼續執行。

方法區用來存儲類元信息、常量和靜態變量等數據,在jdk1.8以前,這部分被稱爲永久代,但從jdk1.8以後,永久代被移除了,用元數據區來取代。

堆空間用來存儲對象實例,通常分爲新生代和老年代,新生代用來存儲新創建的對象,老年代用來存儲長期存儲的對象,新生代又進一步分爲Eden區和Survivor區,這主要根具體使用哪種垃圾蒐集算法有關係,這部分內存區域也是進行jvm優化的主要區域。

自動垃圾收集機制

Java採用自動垃圾收集機制對內存區域進行回收,避免向c語言那樣使用手動回收出現遺漏時,造成內存泄漏。垃圾收集類型主要分爲三種,一種是Young GC,對新生代內存進行回收,一種是Old GC,對老年代內存空間進行回收,Full GC是同時對新生代和老年代進行回收,比較耗時,在進行jvm優化時應該儘量避免Full GC的發生。

對於垃圾對象的標記,主要有兩種算法:

  • 引用計數
  • 可達性分析

其中,引用計數算法無法解決循環依賴問題,容易造成內存泄漏,所以通常會使用可達性分析來實現垃圾對象的標記,可達性分析的思路是從一個被叫做GC Root根的對象開始,向下搜索,如果一個對象與GC Root根對象之間沒有一條鏈路的話,那就說明這個對象時垃圾對象,即這個對象已經不被使用了,GC Root 根對象主要有三種:

  • 線程棧中的遍歷
  • 本地方法棧中的遍歷
  • 靜態變量

在執行GC時,Jvm對不同類型的引用也有不同的處理策略:

  • 強引用:對象不會被回收;
  • 軟引用:當進行Full GC時如果仍然沒有足夠的空間,則將弱引用對象回收;
  • 弱引用:只要發生GC 就會被回收;
  • 虛引用:一般用不到,不用管它;

另外對於重寫了finalize方法的對象,在回收時,會做特殊處理,首先這些對象會被放到一個隊列中,然後jvm會在後臺啓動一個優先級比較低的線程從隊列中拿到這些對象並調用finalize方法,如果在finalize方法中重新與GC Root對象建立了聯繫,那麼這個對象最終不會被回收,否則,就會被GC回收掉,所以,在開發中儘量不要通過覆蓋finalize方法進行一些操作,一方面finalize方法的執行實際不被jvm保證,另外在進行GC時,這些對象會被特殊處理,影響GC效率。

對於Class對象,正常情況下不會被回收,但也不是絕對的,只要能滿足以下條件,方法區中的class對象也是會被回收的:

  • 對應的所有實例對象都已經被回收;
  • Class對象沒有在任何地方被引用到;
  • 加載這個類的ClassLoader也被回收;

Jvm中涉及的垃圾收集算法有三種,分別是標記清除算法、標記整理算法、複製算法。Jvm採用分代收集策略,組合這幾種不同的算法實現垃圾收集。Jvm目前提供的垃圾收集器有Serial GC、ParNew GC、Parallel GC、CMS GC和G1 GC,其中,Serial GC是一種單線程的GC,它可以用在新生代也可以用在老年在,在新生代採用複製算法,在老年代採用標記整理算法,Serial GC在多核CPU中不能夠利用多核的特性,會影響執行效率,這種收集器通常被用在 client 模式下的Jvm中,因爲在client 模式下,需要處理的垃圾對象比較少,這種實現方式反而簡單高效。ParNew GC可以理解爲Serial GC的多線程版,經常被用在新生代,和CMS GC配合使用。Parallel GC是一個能夠控制用戶線程停頓時間(Stop The World)的垃圾收集器,這種特性通常也被稱爲吞吐量優先,在新生代採用複製算法,老年代採用標記整理算法,這種垃圾收集器並不常用。CMS GC是一款老年代垃圾收集器,通常與ParNew GC一起配合使用,採用標記清除算法,當然也可以通過參數-XX:UseCMSCompactAtFullCollection來配置是否進行整理,以防止產生內存碎片,CMS GC的目標是儘量減少用戶線程的停頓時間,它在執行的時候分爲四個步驟:

  • 初始標記:標記所有的GC Root 根對象,會發生Stop The World,但時間非常短;
  • 併發標記:與用戶線程並行執行,從前一步找出的GC Root 根對象開始,查找引用鏈,確定垃圾對象;
  • 重新標記:會出現Stop The World,但是是多線程執行,對第二步用戶線程的影響進行修正;
  • 併發清理:不會出現Stop The World,對垃圾對象進行清理,這裏會產生浮動垃圾,要等到下次GC才能被清理;

另外,在併發標記和併發清理階段,由於是和用戶線程並行執行,所以會對CPU敏感。

另外還有一種在新近引入的GC ,叫G1 GC,這種GC相比於前集中GC,在實現上有一些區別,GC GC被內存區域分成了若干個等大小的Region區域,每個Region區域可以存儲包含Eden、Survivor、Old區的對象,當這些Region內存被回收後,又可以分配其他區域的對象到這個Region,更加靈活。另外,G1 GC的另一個特點是可以通過參數精確控制GC 時間,在G1 GC下,Eden區默認大小爲整個堆內存大小的5%,當被分配滿後,G1會判斷回收這些內存所需時間與用戶配置的GC停頓時間的大小,如果小於用戶配置的GC停頓時間,那麼,就會擴大Eden區的大小,繼續分配對象,直到匹配用戶配置的GC停頓時間,這樣,即提高了GC效率,同時又能保證GC停頓時間可控,基於以上特點,G1 GC一般被用在大內存場景中,例如,32G或64G的服務器,如果此時還採用其他垃圾收集器的話,由於內存空間太大,即使是Young GC,每次話費的時間也會比較長,但G1就可以根據用戶的配置來控制GC時間,確保用戶線程不會被長時間阻塞。

對象的分配策略

  • 大對象直接分配到老年代:大對象的標準由參數指定,如果一個對象的大小達到了配置值,則直接分配在老年代;
  • GC 年齡超過上限的對象直接分配到老年代:默認的GC 年齡上限是15,也可以通過參數進行調整;
  • 執行Young GC後,Survivor放不下的話會直接放到老年代
  • 動態年齡判斷機制:這個規則的意思是,如果分配到Survivor中存活的對象的大小綜合 > Survivor的一半 時,那麼大於等於這批對象年齡最大值的對象都會被放到老年代。例如,例如 年齡1 + 年齡2 + 年齡3 + … + 年齡k + … + 年齡n = Survivor * 80%,年齡1 + … + 年齡k = Survivor * 50 ,那麼,年齡在 k ~ n 之間的對象都會被放到老年代。
  • 老年代分配擔保機制:當發生Young GC時,首先會判斷年輕代對象的總大小是否大於老年代剩餘空間的大小,然後再判斷有沒有開啓老年代分配擔保機制,如果沒有,則直接進行Full GC,如果開啓,則判斷歷次進行Young GC後存活的對象中進入到老年代的對象的平均大小,如果這個值仍然大於老年代剩餘空間,則執行Full GC,否則執行Young GC。

JVM 調優

一般來說,JVM調優的目標有一下幾個:

  • 減少full gc的次數
  • 減少gc的次數
  • 減少gc的執行時間

針對的系統一般有兩種:

  • 新上線的系統,對jvm內存分配策略做評估
  • 以及運行的系統,gc導致用戶線程收到了影響

對於減少full gc的次數的優化,首先要明確什麼情況下會觸發full gc,這就需要結合對象的分配策略來分析,通常,觸發full gc是由於老年代剩餘空間比較小導致的,那麼,我們就要分析什麼情況下,對象會被轉移到老年代:

  • 大對象會被直接分配到老年代:可以通過參數調節大對象的標準,但是需要根據業務系統的特點,確定大對象的範圍,一方面,要避免哪些朝生夕死的較大對象進入老年代,一方面,要讓長期存活的大對象儘快進入老年代,這就需要分析業務對象的特點了。
  • GC年齡默認超過15次的,會被分配到老年代:這裏有優化點需要注意,一是要讓那些確實會長期存活的對象儘早進入老年代,避免在新生代中來回移動,消耗性能,另一方面,要適當增大新生代大小,避免頻繁發生Young GC導致對象GC年齡晉升的過快,最終導致不該進入老年代的對象進入了老年代。
  • Young GC後,如果Survivor放不下存活的對象,這些對象就會被放到老年代,所以,可以通過適當調大Survivor大小,來避免對象過早的晉升到老年代。
  • 動態對象年齡判斷機制:對於這一點,也可以通過適當調大Survivor大小,來避免對象過早的晉升到老年代。
  • 老年代分配擔保機制:可以通過開啓老年代分配擔保機制,來避免在發生Young Gc時,只有新生代對象總大小大於老年代剩餘空間時就觸發Full GC。

在對jvm進行優化時,可以藉助一些java 提供的命令來輔助我們,比如,可以開啓GC log來記錄jvm執行gc的詳細情況,方便對jvm的gc行爲進行分析,另外,還可以通過jmap -heap / jstack -gc 來查看堆內存中各個 區域的使用情況。

對於如何評估jvm 內存配置參數,個人一般的做法是,根據系統的特點,找到業務主線,然後評估出一個主流程走下來所創建的對象的的大小,再根據周邊流程的特點,將結果放到10-20倍,預留出足夠的緩衝空間,然後再評估業務流量,例如業務qps達到500,一個主流程評估下來需要創建 1MB對象,那麼也就是說,每秒鐘會創建
500MB的對象,而這些對象都是些朝生夕死的對象,那麼就需要保證Yong區有足夠的空間,給GC的執行流出餘地,避免因爲Yong區太小,導致頻繁的Yong GC,最終導致大量對象進入老年代觸發Full GC。

另外還需要注意的的,在評估內存分配策略時,還需要考慮,當qps很高,系統壓力比較大時,系統磁盤、網絡等的性能也會有所下降,往往正常1秒鐘處理完的業務,可能需要更長時間,所以也要爲這些預留出足夠的空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章