程序員進階架構師必看的面試重災區:JVM整體架構、內存模型與調優實戰
從今天開始,我們正式開始《架構師進階系列》技術文的更新,在《架構師進階系列》中,我們首先一起來探討有關JVM的知識。
很多小伙伴都認為JVM的知識很難,很枯燥,不知道該如何學習,買了很多關于JVM的書籍,看了沒幾頁就看不下去了,隨后書籍被放到書架里,很長一段時間內成為了“吃灰”的擺件。
其實,在互聯網這個行業中,誰掌握了底層的核心知識,誰就能在激烈的競爭環境中脫穎而出。JVM看起來很難,只要你掌握了學習JVM的規律和方法,吃透它,其實很簡單的。
PS:后續星球帶著大家一起從零開始設計并手寫JVM源碼。
文章總體結構
本文中,我們將按照如下結構介紹JVM的整體架構和調優參數。
JVM的分類
這里,我們先來說說什么是VM吧,VM的中文含義為:虛擬機,指的是使用軟件的方式模擬具有完整硬件系統功能、運行在一個完全隔離環境中的完整計算機系統,是物理機的軟件實現。
常用的虛擬機有:VMWare、Virtual Box,Java Virtual Machine(JVM,Java虛擬機)。
這里,我們重點聊的就是JVM,Java虛擬機。看下圖。
這張圖看起來還是比較簡單的,JVM運行于操作系統之上,操作系統是運行在計算機硬件上的。
關于JVM,其實有很多大廠開發了不同版本的JVM,比較知名的有:Sun HotSpot VM、BEA JRockit VM、IBM J9 VM、 Azul VM、 Apache Harmony、 Google Dalvik VM、 Microsoft JVM等等。
現在使用的比較多的JDK8版本就是Sun HotSpot VM與BEA JRockit VM合并之后開發出的JDK版本。
JVM的構成
JVM主要由三個子系統構成,分別為:類加載器子系統、運行時數據區(內存結構)和字節碼執行引擎。
為了更好的理解JVM,我們來看一下JVM的全貌圖。
當我們開發Java程序時,首先會編寫.java文件,之后,會將.java文件編譯成.class文件。
JVM中,會通過類裝載子系統將.class文件的內容裝載到JVM的運行時數據區,而JVM的運行時數據區又會分為:方法區、堆、棧、本地方法棧和程序計數器 幾個部分。
在裝載class文件的內容時,會將class文件的內容拆分為幾個部分,分別裝載到JVM運行時數據區的幾個部分。其中,值得注意的是:程序計數器的作用是:記錄程序執行的下一條指令的地址。
方法區也叫作元空間,主要包含了:運行時常量池、類型信息、字段信息、方法信息、類加載器的引用、對應的Class實例的引用等信息。
在JVM中,程序的執行是通過執行引擎進行的,執行引擎會調用本地方法的接口來執行本地方法庫,進而完成整個程序邏輯的執行。
我們常說的垃圾收集器是包含在執行引擎中的,在程序的運行過程中,執行引擎會開啟垃圾收集器,并在后臺運行,垃圾收集器會不斷監控程序運行過程中產生的內存垃圾信息,并根據相應的策略對垃圾信息進行清理。
這里,大家需要注意的是:棧、本地方法棧和程序計數器是每個線程運行時獨占的,而方法區和堆是所有線程共享的。所以,棧、本地方法棧和程序計數器不會涉及線程安全問題,而方法區和堆會涉及線程安全問題。
方法區(元空間)
很多小伙伴一看到方法區三個字,腦海中的第一印象可能是存儲方法的地方吧。
實則不然,方法區的另一個名字叫作元空間,相信不少小伙伴或多或少的聽說過元空間。這個區域是JDK1.8中劃分出來的。主要包含:運行時常量池、類型信息、字段信息、方法信息、類加載器的引用、對應的Class實例的引用等信息。方法區中的信息能夠被多個線程共享。
例如,在程序中聲明的常量、靜態變量和有關于類的信息等的引用,都會存放在方法區,而這些引用所指向的具體對象 一般都會在堆中開辟單獨的空間進行存儲,也可能會在直接內存中進行存儲。
堆
堆中主要存儲的是實際創建的對象,也就是會存儲通過new關鍵字創建的對象,堆中的對象能夠被多個線程共享。堆中的數據不需要事先明確生存期,可以動態的分配內存,不再使用的數據和對象由JVM中的GC機制自動回收。對JVM的性能調優一般就是對堆內存的調優。
Java中基本類型的包裝類:Byte、Short、Integer、Long、Float、Double、Boolean、Character類型的數據是存儲在堆中的。
堆一般會被分成年輕代和老年代。而年輕代又會被進一步分為1個Eden區和2個Survivor區。在內存分配上,如果保持默認配置的話,年輕代和老年代的內存大小比例為1 : 2,年輕代中的1個Eden區和2個Survivor區的內存大小比例為:8 : 1 : 1。
棧
棧一般又叫作線程棧或虛擬機棧,一般存儲的是局部變量。在Java中,每個線程都會有一個單獨的棧區,每個棧中的元素都是私有的,不會被其他的棧所訪問。棧中的數據大小和生存期都是確定的,存取速度比較快。
在Java中,所有的基本數據類型(byte、short、int、long、float、double、boolean、char)和引用變量(對象引用)都是在棧中的。一般情況下,線程退出或者方法退出時,棧中的數據會被自動清除。
程序在執行過程中,會在棧中為不同的方法創建不同的棧幀,在棧幀中又包含了:局部變量表、操作數棧、動態鏈接和方法出口。
關于局部變量表、操作數棧、動態鏈接和方法出口的具體作用,會在《架構師進階系列》中的后續文章中詳細闡述。
棧中一般會存儲對象的引用,這些引用所指向的具體對象一般都會在堆中開辟單獨的地址空間進行存儲,也有可能存儲在直接內存中。
注意:這里說的是這些引用所指向的具體對象一般都會在堆中開辟單獨的地址空間進行存儲,也有可能存儲在直接內存中。
因為在JVM中,如果開啟了逃逸分析和標量替換,則可能不會再在堆上創建對象,可能會將對象直接分配到棧上,也可能不再創建對象,而是進一步分解對象中的成員變量,將其直接在棧上分配空間并賦值。
本地方法棧
本地方法棧相對來說比較簡單,就是保存native方法進入區域的地址。
例如,在Java中創建線程,調用Thread對象的start()方法時,會通過本地方法start0()調用操作系統創建線程的方法。此時,本地方法棧就會保存start0()方法進入區域的內存地址。
程序計數器
程序計數器也叫作PC計數器,只要存儲的是下一條將要執行的命令的地址。
JVM調優實戰
在JVM中,主要是對堆(新生代)、方法區和棧進行性能調優。各個區域的調優參數如下所示。
- 堆:-Xms、-Xmx
- 新生代:-Xmn
- 方法區(元空間):-XX:MetaspaceSize、-XX:MaxMetaspaceSize
- 棧(線程):-Xss
為了更加直觀的表述,我們可以將JVM的內存區域和對應的調優參數總結成下圖所示。
在設置JVM啟動參數時,需要特別注意方法區(元空間)的參數設置。
關于方法區(元空間)的JVM參數主要有兩個:-XX:MetaspaceSize和-XX:MaxMetaspaceSize。
-XX:MetaspaceSize: 指的是方法區(元空間)觸發Full GC的初始內存大小(方法區沒有固定的初始內存大小),以字節為單位,默認為21M。達到設置的值時,會觸發Full GC,同時垃圾收集器會對這個值進行修改。
如果在發生Full GC時,回收了大量內存空間,則垃圾收集器會適當降低此值的大小;如果在發生Full GC時,釋放的空間比較少,則在不超過設置的-XX:MetaspaceSize值或者在沒設置-XX:MetaspaceSize的值時不超過21M,適當提高此值。
-XX:MaxMetaspaceSize: 指的是方法區(元空間)的最大值,默認值為-1,不受堆內存大小限制,此時,只會受限于本地內存大小。
最后需要注意的是: 調整方法區(元空間)的大小會發生Full GC,這種操作的代價是非常昂貴的。如果發現應用在啟動的時候發生了Full GC,則很有可能是方法區(元空間)的大小被動態調整了。
所以,為了盡量不讓JVM動態調整方法區(元空間)的大小造成頻繁的Full GC,一般將-XX:MetaspaceSize和-XX:MaxMetaspaceSize設置成一樣的值。例如,物理內存8G,可以將這兩個值設置為256M。
最后,我們一起看下在物理內存8G的情況下,啟動應用程序時,可以設置的JVM參數。當然,我這里給出的是一些經驗值,實際部署到生產環境時,需要經過壓測找到最佳的參數值。
- 啟動SpringBoot
java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar xxx.jar
- 啟動Tomcat(Linux)
在Tomcat bin目錄下catalina.sh文件里配置。
‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
- 啟動Tomcat(Windows)
在Tomcat bin目錄下catalina.bat文件里配置。
‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
總結
今天,我們一起學習了JVM的整體架構和調優參數,主要包括:JVM的總體結構、JVM的分類、JVM的構成和調優參數。你學會了嗎?