2019年Java大廠面試題講解(周陽) 之 JVM參數調優
626
2025-03-31
這篇總結主要是基于我之前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異常。
棧內存溢出主要與??臻g和線程有關,因為棧是線程私有的,如果創建太多線程,內存值超過??臻g上限,也會報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將堆內存劃分成了大小相同的小塊區域,并且將垃圾集中到一個區域,存活對象集中到另一個區域,然后進行收集,防止產生碎片,同時使分配方式更靈活,它還支持根據對象變化預測停頓時間,從而更好地幫用戶解決延遲等問題。
Java JVM
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。