程序員進階必備知識點,一文解析JVM的內存結構,還不弄懂可不太行


程序員進階必備知識點,一文解析JVM的內存結構,還不弄懂可不太行

前言

Jvm的內存結構是由《java虛擬機規範》制定的,《java虛擬機規範》只負責制定標準,具體的實現多種多樣,比如:sun公司的HotSpot、BEA的JRockit、IBM的J9(前兩個目前都已被Oracle收購),另外Apache、Google、微軟等組織或公司都有自己的java虛擬機實現。只是我們目前開發比較常用的是HotSpot。

《java虛擬機規範》與具體的java虛擬機的關係,有點類似於接口和實現類。《java虛擬機規範》負責制定接口,具體的java虛擬機去實現這些接口。我們編寫的java代碼最終會被編譯成class文件,通過java虛擬機解析加載執行。Java語句的“跨平臺性”,其實就是通過java虛擬機實現的,不同的平臺需要實現不同jvm版本(windows版、linux版等)。我們業務代碼只需要開發一份,編譯成同一份class文件,卻可以在windows、linux等不同的操作系統中執行。

這裡提到的class文件的格式、規範也是由《java虛擬機規範》制定的。換句話講不管採用什麼語言編寫的程序,只要按照《java虛擬機規範》編譯成class文件就可以在java虛擬機中執行。比如現在常見的可以在jvm(等同java虛擬機)上運行的語言:Scala、Groovy、Jython等等數十種語言(筆者只使用過這裡列出三種)。這是jvm虛擬機除了“跨平臺性”之外的另一個強大之處—“跨語言性”。

現在越來越多的語言都有基於jvm實現版本,比如:JavaScript對應的Rhino、Lua對應的Luaj、Python對應的Jyhon等等。利用jvm的“跨語言”特性,可以實現不管你使用什麼語言編寫的代碼最終可以在同一個平臺jvm中運行,實現跨語言調用。Jvm不再是java語言的專屬,它屬於世界上個各種編程語言。

為什麼這麼多語言都要爭相實現基於jvm的實現版本呢?前面已經提到兩點:藉助jvm可以實現“跨平臺”;藉助jvm可以實現“跨語言”。還有一點其實跟今天主題相關:藉助jvm實現自動“內存管理”。

眾所周知,與c、c++不同(需要自己控制內存),java可以自動實現垃圾內存回收。其實這份工作不是java語言本身實現的,而是jvm實現的,也就是說任何一種其他語言只要能遵循《java虛擬機規範》可以編譯成可以執行class文件,其內存管理就可以放心的交給jvm。

話又說回來,主流的jvm實現本質上還是使用的c、c++寫的(當然理論上用什麼語言寫都可以,只要符合《java虛擬機規範》),其內存管理還是通過c、c++控制內存空間的開闢和銷燬。只是把這部分工作交給了jvm來做,java程序員只需要關心自己的業務邏輯即可。這有點類似架構設計,架構師把通用的功能提到框架中統一處理,普通程序員只需編寫業務代碼即可。或者可以說Jvm是更頂層的架構設計,只是架構師變成jvm源碼實現那幫傢伙而已。

另外《java虛擬機規範》有多個版本,筆者只閱讀過“周老師”翻譯的《java虛擬機規範 java SE7》,感興趣的可以直接研究oracle官網最新的版本(英文原版)。

Jvm的內存結構

根據java虛擬機規範可以完成java虛擬機的開發,Java虛擬機可以看作是一臺抽象的計算機。如同真實的計算機那樣,它有自己的指令集以及各種運行時內存區域。本次主題是討論jvm的內存結構,對應的就是jvm“運行時內存區”。

該區域大致分為5部分:方法區、java堆、PC寄存器(或者程序計數器)、java虛擬機棧、本地方法棧。大致流程為程序啟動時:先把class文件加載到方法區;初始化bean對象放到java堆(比如spring ioc容器中的bean);每個線程執行時會對應一個自己私有的PC寄存器;同時還會創建一個私有的 “java虛擬機棧”或者“本地方法棧”。

由此可以看出:方法區和java堆是所有線程公有的,PC寄存器、java虛擬機棧和本地方法棧是線程私有的。結構圖如下(來至《深入理解Java虛擬機》):

程序員進階必備知識點,一文解析JVM的內存結構,還不弄懂可不太行

1、方法區

在程序啟動時jvm會讀取class文件,把每個類的結構信息放到“方法區”,這裡的class文件包括jar包、war包中的所有class文件。所以程序中應該儘量避免引入無用的、重複的(不同版本)jar包。類的結構信息包括:運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容、還包括一些在類、實例、接口初始化時用到的特殊方法(<init>和<cinit>方法)。該區域所有線程共享。/<cinit>/<init>

在jdk1.8(對應不同的jvm實現)以前,我們常用的hotspot java虛擬機內存分代中有“永久代”的概念,這個“永久代”等價於“方法區”。可以通過PermSize 和 MaxPermSize來設置“永久代”大小。如果超過MaxPermSize,系統會拋出OutOfMemoryError: PermGen異常。

在jdk1.8之後,hotspot jvm在內存空間中完全移除了“永久代”。“方法區”中“類的元數據信息”被放到“元空間”(Metaspace),“運行時常量池”被放到“java堆”(這部分是從jdk1.7開始)。

在jdk1.8之後,PermSize 和 MaxPermSize參數設置失效,啟動時會有警告信息。改用新的參數MaxMetaspaceSize來限制“元空間”的大小,如果不設置默認最大為本機內存容量(動態調整)。建議通過MaxMetaspaceSize設置最大“元空間”,如果類元數據的空間佔用達到參數“MaxMetaspaceSize”設置的值,將會觸發對死亡對象和類加載器的垃圾回收。

2、java堆

通過-Xms -Xmx指定堆內存大小,Java堆在jvm啟動時創建,所有對象的創建和銷燬都在這個區域進行,是jvm管理的最大的一塊內存(可以不是連續的)。對象的銷燬指的就是垃圾回收,為了更合理的回收對象(對象存活時間的長短),通常的jvm實現把java堆分為年輕代和年老代,並採用不同的垃圾回收算法、以及垃圾回收器進行垃圾回收(對象銷燬)。這裡不對分代算法、垃圾回收器詳細講解。

該區域中的對象是所有線程共享的,典型的運用場景就是spring的ioc容器,在程序啟動時創建一系列對象放到一個全局的map數據結構裡,防止被垃圾回收。線程中可以直接使用這些對象,而不需要重複創建和銷燬。

在創建新對象時,如果該區域已沒有足夠的空間 會拋出OutOfMemoryError異常

3、程序計數器

由於jvm是支持多線程執行的,但本質上是cpu在多個線程之間切換,為了記錄每個線程執行的位置,每個線程都有自己獨有的程序計數器,多個線程之間互不干擾。對於非native方法程序計數器記錄的是正在執行的虛擬機字節碼指令地址,可以看做所執行字節碼的指示器,通過字節解釋器改變其值來保證程序按照特定的順序執行。

PC寄存器的容量至少應當能保存一個returnAddress類型的數據或者一個與平臺相關的本地指針的值,其大小是能確定的。

該區域需要注意以下三點:

1.如果線程正在執行的是非native方法,那麼計數器記錄的是正在執行的虛擬機字節碼指令地址

2.如果執行的native方法,計數器當中的內容應當是空。執行順序交給jvm的native方法控制。

3.該區域是java的虛擬機規範當中,唯一一個沒有規定OutOfMemoryError的區域。

4、java虛擬機棧

每一條java虛擬機線程在創建時會創建自己的java虛擬機棧,因此它是線程私有的。其主要作用是:用於存儲局部變量與一些過程計算結果的地方。其工作方式是結合“程序計數器”讀入變量到棧,根據不同的指令讀取值出棧進行運算(先入後出),運算結果再入棧。

java虛擬機棧的總容量可以動態擴展,但每個線程的棧大小是固定的,可以通過-Xss參數指定。總內存固定其值越小就可以支持創建更多的線程,同時每個棧的容量就越小,當該線程需要的容量超過這個值時,就會拋出StackOverflowError異常。同理 如果-Xss參數指定的值越大,每個線程可以用的棧內存空間就越大可以存放更多的局部變量等信息,但支持的線程就越小,如果總內存耗盡 沒有足夠的空間開闢新的線程,會拋出OutOfMemoryError異常。簡單的講如果拋出“拋出StackOverflowError異常”,增大-Xss的值;如果拋出OutOfMemoryError異常,減小-Xss的值。

Java虛擬機棧裡數據結構叫“棧幀”。

棧幀隨著方法調用而創建,隨著方法結束而銷燬,也就是說一個線程裡執行多個方法,對應會產生和銷燬多個棧幀,這裡的銷燬時機是指方法調用結束,也就是說棧的內存回收不像java堆 沒有複雜的垃圾回收機制。雖然同一個線程裡會有多個棧幀,但同一時間只有一個棧幀,上面提到的-Xms指定的空間,其實是每個棧幀的空間。當線程中一個方法返回時,當前棧幀會傳回此方法的執行結果給前一個棧幀,在方法返回之後,當前棧幀就隨之被丟棄,前一個棧幀就重新成為當前棧幀了。

每個“棧幀”都有自己的:局部變量表、操作數棧、動態鏈接、返回地址。其中局部變量表和操作數棧的容量是在編譯期確定,因此棧幀實際消耗容量的大小僅僅取決於Java虛擬機的實現和方法調用時可被分配的內存。

局部變量表:存放局部變量的列表,一個局部變量類型為boolean、byte、char、short、float、reference和returnAddress的數據,兩個局部變量可以保存一個類型為long和double的數據。局部變量使用索引來進行定位訪問,第一個局部變量的索引值為零。

操作數棧:後進先出(Last-In-First-Out,LIFO)棧,長度由編譯期決定,在任意時刻,即任意一個棧幀中的操作數棧都會有一個確定的棧深度,一個long或者double類型的數據會佔用兩個單位的棧深度,其他數據類型則會佔用一個單位深度。

動態鏈接:簡單的理解為指向運行時常量池的引用。在Class文件裡面,描述一個方法調用了其他方法,或者訪問其成員變量是通過符號引用(Symbolic Reference)來表示的,動態鏈接的作用就是將這些符號引用所表示的方法轉換為實際方法的直接引用。

返回地址:方法調用的返回,包括正常返回(有返回值)和異常返回(沒有返回值),不同的返回類型有不同的指令。

5、本地方法棧

用於支持native方法,和java虛擬機棧相似,是線程私有,只是這個棧是採用其他語言實現。同樣會有可能拋出StackOverflowError、OutOfMemoryError異常。-Xss設置棧內存的大小同樣適用於本地方法棧。

關於內存溢出和內存洩漏

內存洩漏一定會導致內存溢出,但內存溢出不一定是內存洩漏導致,也有可能是服務器內存本來就不足,可以通過增加服務器內存 同時增大-Xms –Xmx配置。

內存洩漏一般比較隱蔽,難於發現。典型的發生場景就是,多線程的的線程中中使用ThreadLocal,在線程執行結束時沒有remove,導致對象無法被回收,日積月累內存耗盡,拋出OutOfMemoryError異常。

關於jvm的內存結構就總結到這裡。

更多資源獲取方式:轉發+轉發+私信[資料]即可獲取免費領取方式!


分享到:


相關文章: