JVM 簡單入門 概述

一文看懂 JVM內存劃分

1. 概述

對於從事C、C++程序開發的開發人員來說,在內存管理領域,他們既是擁有最高權力的“皇帝”, 又是從事最基礎工作的勞動人民——既擁有每一個對象的“所有權”,又擔負着每一個對象生命從開始 到終結的維護責任。

對於Java程序員來說,在虛擬機自動內存管理機制的幫助下,不再需要爲每一個new操作去寫配delete/free代碼,不容易出現內存泄漏和內存溢出問題,看起來由虛擬機管理內存一切都很美好。不 過,也正是因爲Java程序員把控制內存的權力交給了Java虛擬機,一旦出現內存泄漏和溢出方面的問 題,如果不瞭解虛擬機是怎樣使用內存的,那排查錯誤、修正問題將會成爲一項異常艱難的工作。

2. 運行時的數據區域( HotSpot虛擬機)

要想了解jvm怎麼執行java程序幫我們回收內存,首先要先了解jvm執行是的數據區域。Java 虛擬機在執行 Java 程序的過程中會把它管理的內存劃分成若干個不同的數據區域。

JDK1.8和之前版本不同,看圖。從JDK1.6開始就開始去方法區(永久代)化,到1.8之後出現了元空間

JDK 1.8 之前

img

JDK1.8:

img

2.1 程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執行的 字節碼的行號指示器。在Java虛擬機的概念模型裏,字節碼解釋器工作時就是通過改變這個計數器

的值來選取下一條需要執行的字節碼指令,它是程序控制流的指示器,分支、循環、跳轉、異常處

理、線程恢復等基礎功能都需要依賴這個計數器來完成。

此區域線程私有,唯一一個沒有OutOfMemoryError情況的區域。

2.2 虛擬機棧 和 本地方法棧

​ 本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別只是虛擬機 棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的本地(Native) 方法服務。

​ 與程序計數器一樣,方法棧也是線程私有的,生命週期和線程相同。虛擬機棧描述的是Java方法執行的線程內存模型:每個方法被執行的時候,Java虛擬機都 會同步創建一個棧幀(Stack Frame)用於存儲局(部變量表、操作數棧、動態連接、方法出口)等信 息。每一個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

​ 局部變量表存放了編譯期可知的各種Java虛擬機基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它並不等同於對象本身,可能是一個指向對象起始 地址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress 類型(指向了一條字節碼指令的地址)。

​ 這些數據類型在局部變量表中的存儲空間以局部變量槽(Slot)來表示,其中64位長度的long和 double類型的數據會佔用兩個變量槽,其餘的數據類型只佔用一個。局部變量表所需的內存空間在編 譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的局部變量空間是完全確定 的,在方法運行期間不會改變局部變量表的大小。請讀者注意,這裏說的“大小”是指變量槽的數量, 虛擬機真正使用多大的內存空間(譬如按照1個變量槽佔用32個比特、64個比特,或者更多)來實現一 個變量槽,這是完全由具體的虛擬機實現自行決定的事情。

​ HotSpot虛擬機的棧容量是不可以動態擴展的,那麼當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,新的棧幀內存無法分配,就拋出 StackOverFlowError 錯誤。當新的線程無法被創建分配內存時,會拋出OutOfMemoryError異常。

相關命令

  • -Xss128k: 設置每個線程的可用棧內存大小 。 減少這個值可以增加生成的線程數量,增加值可以增大可用的方法棧深度。

    -Xss128k可以正常用於32位Windows系統下的JDK 6,但 是如果用於64位Windows系統下的JDK 11,則會提示棧容量最小不能低於180K,而在Linux下這個值則 可能是228K,

2.3. java 堆(heap)

​ 對於Java應用程序來說,Java堆(Java Heap)是虛擬機所管理的內存中最大的一塊。Java堆是被所 有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,Java 世界裏“幾乎”所有的對象實例都在這裏分配內存。現在 1.8 常量池也是放在堆中了,之前是在方法區(永久代)中。

如果在Java堆中沒有內存分配給實例,並且堆也無法再 擴展時,Java虛擬機將會拋出OutOfMemoryError異常。

相關命令

  • -Xms1024m:初始堆大小

  • -Xmx1024m:最大堆大小

    一般情況將堆的最小值-Xms參數與最大值-Xmx參數 設置爲一樣即可避免堆自動擴展和JVM重新分配內存。

2.4. 方法區(運行時常量池)/ 元數據空間

方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載 的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。

說到方法區,不得不提一下“永久代”這個概念,尤其是在JDK 8以前,許多Java程序員都習慣在 HotSpot虛擬機上開發、部署程序,很多人都更願意把方法區稱呼爲“永久代”(Permanent Generation),或將兩者混爲一談。本質上這兩者並不是等價的,因爲僅僅是當時的HotSpot虛擬機設 計團隊選擇把收集器的分代設計擴展至方法區,或者說使用永久代來實現方法區而已,這樣使得 HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分內存,省去專門爲方法區編寫內存管理代碼的 工作。這種設計導致了Java應用更容易遇到 內存溢出的問題

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字 段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table),用於存放編譯期生 成的各種字面量與符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。

對象創建的時候回去常量池查找類的符號引用 ,沒有就會執行類加載過程。

相關命令:

-XX:PermSize=6m-XX:MaxPermSize=6m 限制永久代(方法區)的初始化大小和最大大小。

永久代的內存時不會被java回收機制回收的,分配要謹慎

JDK8 以後永久代被拋棄,出現了元數據空間。其配置

-XX:MaxMetaspaceSize=6m:設置元空間最大值,默認是-1,即不限制,或者說只受限於本地內存

大小。

-XX:MetaspaceSize=6m:指定元空間的初始空間大小,以字節爲單位,達到該值就會觸發垃圾收集 進行類型卸載,同時收集器會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放 了很少的空間,那麼在不超過-XX:MaxMetaspaceSize(如果設置了的話)的情況下,適當提高該值。

2.5. 直接內存

直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是《Java虛擬機規範》中 定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現。

在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了 在Java堆和Native堆中來回複製數據。

顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,則肯定還是會受到 本機總內存(包括物理內存、SWAP分區或者分頁文件)大小以及處理器尋址空間的限制,一般服務 器管理員配置虛擬機參數時,會根據實際內存去設置-Xmx等參數信息,但經常忽略掉直接內存,使得 各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現 OutOfMemoryError異常。

3. HotSpot虛擬機對象創建流程

3.1 對象創建流程

  • 類加載檢查

    當Java虛擬機遇到一條字節碼new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到 一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那 必須先執行相應的類加載過程。

  • 分配內存

    爲對象分配空間的任務實際上便等同於把一塊確定 大小的內存塊從java堆中劃分出來。

  • 初始化零值

    內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

  • 設置對象頭

    初始化零值完成之後,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不同,如是否啓用偏向鎖等,對象頭會有不同的設置方式。

  • 執行init方法

    在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建纔剛開始,方法還沒有執行,所有的字段都還爲零。所以一般來說,執行 new 指令之後會接着執行 方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。

3.2 對象的內存佈局

在 Hotspot 虛擬機中,對象在內存中的佈局可以分爲 3 塊區域:對象頭實例數據對齊填充

Hotspot 虛擬機的對象頭包括兩部分信息第一部分用於存儲對象自身的運行時數據(哈希碼、GC 分代年齡、鎖狀態標誌等等),另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。

實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內容。

對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位作用。 因爲 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

3.3 對象的訪問定位

建立對象就是爲了使用對象,我們的 Java 程序通過棧上的 reference 數據來操作堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有①使用句柄②直接指針兩種:

  1. 句柄: 如果使用句柄的話,那麼 Java 堆中將會劃分出一塊內存來作爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;

  2. 直接指針: 如果使用直接指針訪問,那麼 Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而 reference 中存儲的直接就是對象的地址。

這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

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