天天看點

JVM原理學習總結

#JVM原理學習總結

這篇總結主要是基于我之前JVM系列文章而形成的的。主要是把重要的知識點用自己的話說了一遍,可能會有一些錯誤,還望見諒和指點。謝謝

#更多詳細内容可以檢視我的專欄文章:深入了解JVM虛拟機

https://blog.csdn.net/column/details/21960.html

JVM介紹和源碼

首先JVM是一個虛拟機,當你安裝了jre,它就包含了jvm環境。JVM有自己的記憶體結構,位元組碼執行引擎,是以class位元組碼才能在jvm上運作,除了Java以外,Scala,groovy等語言也可以編譯成位元組碼而後在jvm中運作。JVM是用c開發的。

JVM記憶體模型

記憶體模型老生常談了,主要就是線程共享的堆區,方法區,本地方法棧。還有線程私有的虛拟機棧和程式計數器。

堆區存放所有對象,每個對象有一個位址,Java類jvm初始化時加載到方法區,而後會在堆區中生成一個Class對象,來負責這個類所有執行個體的執行個體化。

棧區存放的是棧幀結構,棧幀是一段記憶體空間,包括參數清單,傳回位址,局部變量表等,局部變量表由一堆slot組成,slot的大小固定,根據變量的資料類型決定需要用到幾個slot。

方法區存放類的中繼資料,将原來的字面量轉換成引用,當然,方法區也提供常量池,常量池存放-128到127的數字類型的包裝類。

字元串常量池則會存放使用intern的字元串變量。

JVM OOM和記憶體洩漏

這裡指的是oom和記憶體洩漏這類錯誤。

oom一般分為三種,堆區記憶體溢出,棧區記憶體溢出以及方法區記憶體溢出。

堆記憶體溢出主要原因是建立了太多對象,比如一個集合類死循環添加一個數,此時設定jvm參數使堆記憶體最大值為10m,一會就會報oom異常。

棧記憶體溢出主要與棧空間和線程有關,因為棧是線程私有的,如果建立太多線程,記憶體值超過棧空間上限,也會報oom。

方法區記憶體溢出主要是由于動态加載類的數量太多,或者是不斷建立一個動态代理,用不了多久方法區記憶體也會溢出,會報oom,這裡在1.7之前會報permgem oom,1.8則會報meta space oom,這是因為1.8中删除了堆中的永久代,轉而使用中繼資料區。

記憶體洩漏一般是因為對象被引用無法回收,比如一個集合中存着很多對象,可能你在外部代碼把對象的引用置空了,但是由于對象還被集合給引用着,是以無法被回收,導緻記憶體洩漏。測試也很簡單,就在集合裡添加對象,添加完以後把引用置空,循環操作,一會就會出現oom異常,原因是記憶體洩漏太多了,導緻沒有空間配置設定新的對象。

常見調試工具

指令行工具有jstack jstat jmap 等,jstack可以跟蹤線程的調用堆棧,以便追蹤錯誤原因。

jstat可以檢查jvm的記憶體使用情況,gc情況以及線程狀态等。

jmap用于把堆棧快照轉儲到檔案系統,然後可以用其他工具去排查。

visualvm是一款很不錯的gui調試工具,可以遠端登入主機以便通路其jvm的狀态并進行監控。

class檔案結構

class檔案結構比較複雜,首先jvm定義了一個class檔案的規則,并且讓jvm按照這個規則去驗證與讀取。

開頭是一串魔數,然後接下來會有各種不同長度的資料,通過class的規則去讀取這些資料,jvm就可以識别其内容,最後将其加載到方法區。

JVM的類加載機制

jvm的類加載順序是bootstrap類加載器,extclassloader加載器,最後是appclassloader使用者加載器,分别加載的是jdk/bin ,jdk/ext以及使用者定義的類目錄下的類(一般通過ide指定),一般核心類都由bootstrap和ext加載器來加載,appclassloader用于加載自己寫的類。

雙親委派模型,加載一個類時,首先擷取目前類加載器,先找到最高層的類加載器bootstrap讓他嘗試加載,他如果加載不了再讓ext加載器去加載,如果他也加載不了再讓appclassloader去加載。這樣的話,確定一個類型隻會被加載一次,并且以高層類加載器為準,防止某些類與核心類重複,産生錯誤。

defineclass findclass和loadclass

類加載classloader中有兩個方法loadclass和findclass,loadclass遵從雙親委派模型,先調用父類加載的loadclass,如果父類和自己都無法加載該類,則會去調用findclass方法,而findclass預設實作為空,如果要自定義類加載方式,則可以重寫findclass方法。

常見使用defineclass的情況是從網絡或者檔案讀取位元組碼,然後通過defineclass将其定義成一個類,并且傳回一個Class對象,說明此時類已經加載到方法區了。當然1.8以前實作方法區的是永久代,1.8以後則是元空間了。

JVM虛拟機位元組碼執行引擎

jvm通過位元組碼執行引擎來執行class代碼,他是一個棧式執行引擎。這部分内容比較高深,在這裡就不獻醜了。

編譯期優化和運作期優化

編譯期優化主要有幾種

1 泛型的擦除,使得泛型在編譯時變成了實際類型,也叫僞泛型。

2 自動拆箱裝箱,foreach循環自動變成疊代器實作的for循環。

3 條件編譯,比如if(true)直接可得。

運作期優化主要有幾種

1 JIT即時編譯

Java既是編譯語言也是解釋語言,因為需要編譯代碼生成位元組碼,而後通過解釋器解釋執行。

但是,有些代碼由于經常被使用而成為熱點代碼,每次都編譯太過費時費力,幹脆直接把他編譯成本地代碼,這種方式叫做JIT即時編譯處理,是以這部分代碼可以直接在本地運作而不需要通過jvm的執行引擎。

2 公共表達式擦除,就是一個式子在後面如果沒有被修改,在後面調用時就會被直接替換成數值。

3 數組邊界擦除,方法内聯,比較偏,意義不大。

4 逃逸分析,用于分析一個對象的作用範圍,如果隻局限在方法中被通路,則說明不會逃逸出方法,這樣的話他就是線程安全的,不需要進行并發加鎖。

1

JVM的垃圾回收

1 GC算法:停止複制,存活對象少時适用,缺點是需要兩倍空間。标記清除,存活對象多時适用,但是容易産生随便。标記整理,存活對象少時适用,需要移動對象較多。

2 GC分區,一般GC發生在堆區,堆區可分為年輕代,老年代,以前有永久代,現在沒有了。

年輕代分為eden和survior,新對象配置設定在eden,當年輕代滿時觸發minor gc,存活對象移至survivor區,然後兩個區互換,等待下一場gc,

當對象存活的門檻值達到設定值時進入老年代,大對象也會直接進入老年代。

老年代空間較大,當老年代空間不足以存放年輕代過來的對象時,開始進行full gc。同時整理年輕代和老年代。

一般年輕代使用停止複制,老年代使用标記清除。

3 垃圾收集器

serial串行

parallel并行

它們都有年輕代與老年代的不同實作。

然後是scanvage收集器,注重吞吐量,可以自己設定,不過不注重延遲。

cms垃圾收集器,注重延遲的縮短和控制,并且收集線程和系統線程可以并發。

cms收集步驟主要是,初次标記gc root,然後停頓進行并發标記,而後處理改變後的标記,最後停頓進行并發清除。

g1收集器和cms的收集方式類似,但是g1将堆記憶體劃分成了大小相同的小塊區域,并且将垃圾集中到一個區域,存活對象集中到另一個區域,然後進行收集,防止産生碎片,同時使配置設定方式更靈活,它還支援根據對象變化預測停頓時間,進而更好地幫使用者解決延遲等問題。

JVM的鎖優化

在Java并發中講述了synchronized重量級鎖以及鎖優化的方法,包括輕量級鎖,偏向鎖,自旋鎖等。詳細内容可以參考我的專欄:Java并發技術指南

微信公衆号

個人公衆号:程式員黃小斜

微信公衆号【程式員黃小斜】新生代青年聚集地,程式員成長充電站。作者黃小斜,職業是阿裡程式員,身份是斜杠青年,希望和更多的程式員交朋友,一起進步和成長!專注于分享技術、面試、職場等成長幹貨,這一次,我們一起出發。

關注公衆号後回複“2019”領取我這兩年整理的學習資料,涵蓋自學程式設計、求職面試、算法刷題、Java技術學習、計算機基礎和考研等8000G資料合集。

JVM原理學習總結

技術公衆号:Java技術江湖

微信公衆号【Java技術江湖】一位阿裡 Java 工程師的技術小站,專注于 Java 相關技術:SSM、SpringBoot、MySQL、分布式、中間件、叢集、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術幹貨和學習經驗,緻力于Java全棧開發!

關注公衆号後回複“PDF”即可領取200+頁的《Java工程師面試指南》強烈推薦,幾乎涵蓋所有Java工程師必知必會的知識點。

JVM原理學習總結