程序運行時,JVM內存到底是如何進行分配的?

2020-12-27 江之眼

Java虛擬機在執行Java程序的過程中,會把它所管理的內存劃分為不同的數據區域。下面這張圖描述了一個HelloWorld.java文件被JVM加載到內存中的過程。

1、HelloWorld.java文件首先需要經過編譯器編譯,生成HelloWorld.class字節碼文件。

2、Java程序中訪問HelloWorld這個類時,需要通過ClassLoader(類加載器)將HelloWorld.class加載到JVM的內存中。

3、JVM中的內存可以劃分為若干個不同的數據區域,主要分為:程序計數器、虛擬機棧、本地方法棧、堆、方法區。

圖1 JVM運行時內存分布

程序計數器

Java程序是多線程的,CPU可以在多個線程中分配執行時間片段。當某一個線程被CPU掛起時,需要記錄代碼已經執行到的位置,方便CPU重新執行此線程時,知道從哪行指令開始執行。這就是程序計數器的作用。

「程序計數器」是虛擬機中一塊較小的內存空間,主要用於記錄當前線程執行的位置。

如下圖所示:每個線程都會記錄一個當前方法執行到的位置,當CPU切換回某一個線程上時,則根據程序計數器記錄的數字,繼續向下執行指令。

圖2 程序計數器記錄位置

實際上除了上圖演示的恢復線程操作之外,其它一些我們熟悉的分支操作、循環操作、跳轉、異常處理等也都需要依賴這個計數器來完成。

關於程序計數器還有幾點需要格外注意:

1、在Java虛擬機規範中,對程序計數器這一區域沒有固定任何OutOfMemoryError情況。

2、線程私有的,每條線程內部都有一個私有程序計數器。它的生命周期隨著線程的創建而創建,隨著線程的結束而死亡。

3、當一個線程正在執行一個Java方法的時候,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址。如果正在執行的是Native方法,這個計數器值則為空(Undefined)。

虛擬機棧

虛擬機棧也是線程私有的,與線程的生命周期同步。在Java虛擬機規範中,對這個區域規定了兩種異常情況:

1、StackOverflowError:當線程請求棧深度超出虛擬機所允許的深度時拋出。

2、OutOfMemoryError:當Java虛擬機動態擴展到無法申請足夠內存時拋出。

在我們學習Java虛擬機的過程中,經常會看到一句話:JVM是基於棧的解釋器執行的,DVM是基於寄存器的解釋器執行的

上面這句話裡的「基於棧」指的就是虛擬機棧。虛擬機棧的初衷是用來描述Java方法執行的內存模型,每個方法執行被執行的時候,JVM都會在虛擬機棧中創建一個棧幀,接下來看下這個棧幀是什麼。

棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,每一個線程在執行某個方法時,都會為這個方法創建一個棧幀。

我們可以這樣理解:一個線程包含多個棧幀,而每個棧幀內部包含局部變量表操作數棧動態連結返回地址等。如下圖所示:

圖3 棧幀

局部變量表局部變量表是變量值的存儲空間,我們調用方法時傳遞的參數,以及在方法內部創建的局部變量都保存在局部變量表中。在Java編譯成class文件的時候,就會在方法的Code屬性表中的max_locals數據項中,確定該方法需要分配的最大局部變量表的容量。如下代碼所示:

public static int add(int k) { int i = 1; int j = 2; return i + j + k;}使用javap -v反編譯之後,得到如下字節碼指令:

public static int add(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: iconst_1 1: istore_1 2: iconst_2 3 istore_2 4: iload_1 5: iload_2 6: iadd 7: iload_0 8: iadd 9: ireturn上面的locals=3就是局部變量表長度時3,也就是說經過編譯之後,局部變量表的長度已經確定為3,分別保存:參數k和局部變量i、j。

注意:系統不會為局部變量賦予初始值(實例變量和類變量都會被賦予初始值),也就是說不存在類變量那樣的準備階段。

操作數棧操作數棧(Operand Stack)也常稱為操作棧,它是一個後入先出棧(LIFO)。

同局部變量表一樣,操作數棧的最大深度也在編譯的時候寫入方法的Code屬性表中的max_stacks數據項中。棧中的元素可以是任意Java數據類型,包括long和double。

當一個方法剛剛開始執行的時候,這個方法的操作數棧是空的。在方法執行的過程中,會有各種字節碼指令被壓入和彈出操作數棧(比如:iadd指令就是將操作數棧中的兩個元素彈出,執行加法運算,並將結果重新壓回到操作數棧中)。

動態連結動態連結的主要目的是為了支持方法調用過程中的動態連接(Dynamic Linking)。

在一個class文件中,一個方法要調用其它方法,需要將這些方法的符號引用轉化為其所在內存地址中的直接引用,而符號引用存在於方法區中。

Java虛擬機中,每個棧幀都包含一個指向運行時常量池中該棧所屬方法的符號引用,持有這個引用的目的就是為了支持方法調用過程中的動態連接(Dynamic Linking)

返回地址當一個方法開始執行後,只有兩種方法可以退出這個方法:

1、正常退出:指方法中的代碼正常完成,或者遇到任何一個方法返回的字節碼指令(如return)並退出,沒有拋出任何異常。

2、異常退出:指方法執行過程中遇到異常,並且這個異常在方法體內部沒有得到處理,導致方法退出。

無論當前方法採用何種方式退出,在方法退出後都需要返回到方法被調用的位置。程序才能繼續執行。而虛擬機棧中的「返回地址」就是用來幫助當前方法恢復它的上層方法執行狀態。

一般來說,方法正常退出時,調用者的PC計數值可以作為返回地址,棧幀中可能保存此計數值。而方法異常退出時,返回地址是通過異常處理器表確定的,棧幀中一般不會保存此部分信息。

實例講解

我用一個簡單的add()方法來演示,代碼如下:

public int add() { int i = 1; int j = 2; int result = i + j; return result + 10;}我們經常會使用javap命令來查看某個類的字節碼指令,比如add()方法的代碼,經過javap之後的字節碼指令如下:

0: iconst_1 (把常量 1 壓入操作數棧棧頂)1: istore_1 (把操作數棧棧頂的出棧放入局部變量表索引為 1 的位置)2: iconst_2 (把常量 2 壓入操作數棧棧頂)3: istore_2 (把操作數棧棧頂的出棧放入局部變量表索引為 2 的位置)4: iload_1 (把局部變量表索引為 1 的值放入操作數棧棧頂)5: iload_2 (把局部變量表索引為 2 的值放入操作數棧棧頂)6: iadd (將操作數棧棧頂的和棧頂下面的一個進行加法運算後放入棧頂)7: istore_3 (把操作數棧棧頂的出棧放入局部變量表索引為 3 的位置)8: iload_3 (把局部變量表索引為 3 的值放入操作數棧棧頂)9: bipush 10 (把常量 10 壓入操作數棧棧頂)11: iadd (將操作數棧棧頂的和棧頂下面的一個進行加法運算後放入棧頂)12: ireturn (結束)從上面字節碼指令也可以看出,其實局部變量表和操作數棧在代碼執行期間是協同合作來達到某一運算效果的。接下來通過圖示來看下這幾行代碼執行期間,虛擬機棧的實際情況。

首先說一下各個指令代表什麼意思:

iconstbipush,這兩個指令都是將常量壓入操作數棧頂,區別就是:當int取值-1~5採用iconst指令,取值-128~127採用bipush指令。istore 將操作數棧頂的元素放入局部變量表的某索引位置,比如istore_5代表將操作數棧頂元素放入局部變量表下標為5的位置。iload 將局部變量表中下標上的值加載到操作數棧頂中,比如iload_2代表將局部變量表索引為2上的值壓入操作數棧頂。首先在Add.java被編譯成Add.class的時候,棧幀中需要多大的局部變量表,多深的操作數棧都已經完全確定了,並且寫入到了方法表的Code屬性中。因此這會局部變量表的大小是確定的,add()方法中有3個局部變量,因此局部變量表的大小是3,但是操作數棧此時為空。

字節碼指令執行過程

最後執行return指令,將操作數棧頂的元素13返回給上層方法。至此add()方法執行完畢。局部變量表和操作數棧也會相繼被銷毀。

本地方法棧

本地方法棧和上面介紹的虛擬機棧基本相同,只不過是針對本地(native)方法。在開發中如果涉及JNI可能接觸本地方法棧多一些,在遊戲虛擬機的實現中已經將兩個合二為一了(比如HotSpot)。

關於堆的更多介紹參考這篇文章:Java垃圾回收機制(GC)-垃圾回收發生的地點在哪?

方法區

方法區(Method Area)也是JVM規範裡規定的一塊運行時區域。方法區主要是存儲已經被JVM加載的類信息(版本、方法、欄位、接口)、常量、靜態變量、即時編譯器編譯後的代碼和數據。該區域同堆一樣,也是被各個線程共享的內存區域。

注意:關於方法區,很多開發者會將其跟「永久區」混淆。所以我在這裡對這兩個概念進行對比一下:

方法區是JVM規範中規定的一塊區域,但是並不是實際實現,切忌將規範跟實現混為一談,不同的JVM廠商可以有不同版本的「方法區」的實現。HotSpot在JDK1.7以前使用「永久區」(Perm區)來實現方法區,在JDK1.8之後「永久區」就已經被移除了,取而代之的是一個叫做「元空間(metaspace)」的實現方式。方法區是規範層面的東西,規定了這一個區域要存放哪些數據。永久區或metaspace是對方法區的不同實現,是實現層面的東西。總結

對於 JVM 運行時內存布局,我們需要始終記住一點:上面介紹的這 5 塊內容都是在 Java 虛擬機規範中定義的規則,這些規則只是描述了各個區域是負責做什麼事情、存儲什麼樣的數據、如何處理異常、是否允許線程間共享等。千萬不要將它們理解為虛擬機的「具體實現」,虛擬機的具體實現有很多,比如 Sun 公司的 HotSpot、JRocket、IBM J9、以及我們非常熟悉的 Android Dalvik 和 ART 等。這些具體實現在符合上面 5 種運行時數據區的前提下,又各自有不同的實現方式。

最後我們藉助一張圖來概括一下本課時所介紹的內容:

圖4 JVM內存結構

總結來說,JVM 的運行時內存結構中一共有兩個「棧」和一個「堆」,分別是:Java 虛擬機棧和本地方法棧,以及「GC堆」和方法區。除此之外還有一個程序計數器,但是我們開發者幾乎不會用到這一部分,所以並不是重點學習內容。 JVM 內存中只有堆和方法區是線程共享的數據區域,其它區域都是線程私有的。並且程序計數器是唯一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 情況的區域。

相關焦點

  • 這一定是全網寫JVM最好的文章之一—JVM運行時數據區
    JVM基礎知識一個Java程序到底是如何運行的?一個Java程序,首先要經過javac編譯成.class文件,.class文件是給JVM進行識別的,JVM將.class文件加載到方法區,執行引擎會執行這些字節碼,執行時,會翻譯成作業系統相關的函數。
  • jvm之棧、堆
    請看下圖:2jvm.png 簡單說來,JVM的工作就是通過類加載系統將字節碼文件加載到內存當中去,加載到內存當中的數據,就從邏輯上形成了我們看到的圖中的運行時數據區(內存模型),隨後執行引擎操作/調度內存模型中數據執行程序。
  • 微信小程序運行內存不足怎麼解決?微信小程序運行內存不足的解決...
    相信很多朋友都有在使用手機微信,那麼大家在使用手機微信小程序的時候,是否有過提示內存不足,無法使用小程序的情況呢?關於這個問題,接下來小編就和大家分享一下我的解決方法,希望能夠幫助到大家。在打開微信小程序或小遊戲的時候,有時會出現「運行內存不足」類似的提示,導致無法正常打開使用。那麼用戶要如何解決這個問題呢?
  • 滿滿的一整篇,全是 JVM 核心知識點!
    不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過參數來指定元空間的大小。1.2、直接內存區域直接內存:一般使用 Native 函數操作 C++代碼來實現直接分配堆外內存,不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。
  • 「GC系列」JVM堆內存分代模型及常見的垃圾回收器
    內存分代模型為什麼要說JVM的內存分代模型呢,因為內存分代和垃圾回收器的運行是有關係的。現在大部分用到的垃圾回收器在邏輯上是分代的,除了G1之外的其他垃圾回收器在邏輯上和物理上都是分代的。「幾個GC的概念」MinorGC/YGC 新生代空間耗盡時觸發的垃圾回收。MajorGC/FullGC 在老年代無法繼續分配空間時觸發,新生代、老年代同時進行垃圾回收。
  • 科普:手機中運行內存和機身內存的關係和區別
    一、手機的「運行內存」和「機身內存」有什麼區別?運行內存的功能:手機運行程序時,程序加載到運行內存中,然後提供給CPU、GPU等硬體來讀取數據,是臨時性存儲,斷電後,數據全部消失。只有所有運行的程序包括作業系統都會先加載到運存裡,CPU等硬體才能讀取指令進行一系列的操作。機身內存的功能:主要是用來儲存資料的,所以機身內存越大越好,可以通過複製、粘貼、刪除等操作來管理存儲文件或者資料。就算手機斷電,儲存在手機上的文件資料也不會消失,機身內存的絕大多數都由存取速度較快的晶片組成。
  • 如何優化生產環境下的Kubernetes資源分配
    我使用kubectl describe進行一些調查後了解到,由於OOMKilled即內存不足,Kubelet在終止pod。我深入探究後意識到,我從另一個部署複製粘貼YAML時,設置了一些限制性過強的內存限制。這段經歷讓我開始思考如何有效地設置請求和限額。
  • 買手機時,內存到底選多少合適?別再花些冤枉錢了!
    教授通過和同事交流發現,很多人換手機竟然是因為內存不足導致的。這讓教授很困惑,為什麼會出現這種情況?最大的原因,就是不知道自己適合使用多大的內存。所以,針對這個問題,會多方面的跟大家講一講如何選擇合適的手機內存組合,避免出現內存不足及內存過剩的情況。
  • mac如何清理內存,好用的mac內存清理工具分享
    想要一款專業、好用的內存清理工具?小編精心整理了幾款功能強大的清理內存工具,幫助你清理不必要後臺程序,以達到釋放內存以保持Mac快速運行的目的。需要的快來一起看看吧~mac如何清理內存,好用的mac內存清理工具分享Memory Clean 3 for Mac(清理內存工具)Memory Clean是優化Mac內存的終極應用程式,最適合在使用內存(RAM)密集型應用程式或遊戲後使用。它複製了重新啟動系統的感覺。
  • 單片機上運行Python-MicroPython(三)
    堆空間當運行中的程序實例化對象時,所需的RAM資源會從一個固定大小的內存池中被申請和分配,該內存池即為堆空間。當對象超過作用域後(或者說其不再可用時),該對象即被稱作"垃圾"資源。此時,一個單獨的進程,被稱作"垃圾回收器",會收回其所佔內存並將其內存返回到可用的堆空間中。該進程會在後臺自動運行,你也可以直接顯式地使用gc.collect()對該過程進行調用。
  • 手機運行內存越大越好?4GB的蘋果和8GB的安卓差別到底在哪裡
    現在很多安卓機型的運行內存都達到了8GB,然而我們再來看看蘋果這邊,蘋果向來都不太注重運行內存,所以在機型的參數表上,一般很少看到iPhone機型的運行內存,小編經過幾番查詢,才搞清楚以下這幾款較新機型的真正運行內存……
  • 乾貨——聊聊內存那些事(基於單片機系統)
    而RAM是隨機讀/寫存儲器,用作數據存儲器,是在運行程序時,存放數據的。>:靜態變量和全局變量的存儲區域是一起的,一旦靜態區的內存被分配, 靜態區的內存直到程序全部結束之後才會被釋放 l  堆區:由程式設計師調用malloc()函數來主動申請的,需使用free()函數來釋放內存,若申請了堆區內存,之後忘記釋放內存,很容易造成內存洩漏
  • 「JVM第八篇——垃圾回收」GC和GC算法
    如果不進行垃圾回收,釋放內存,則內存遲早會被消耗完畢,最終導致程序崩潰。因為程序在運行過程中是會不斷產生對象來佔用內存的。除了釋放成為垃圾的對象,垃圾回收有時也可以清理內存裡的內存碎片,使其能在物理空間上能連成一片,以便JVM能將內存分配給新的對象。
  • iPhone到底要不要關閉後臺程序
    那到底是經常關閉 後臺App 會不會導致耗電還是省電?當掛起時,程序還是停留在內存中的,當系統內存低時,系統就把掛起的程序清除掉,為前臺程序提供更多的內存看一下iPhone運行內存狀態圖一是剛開機後的內存分布狀態圖二是正常使用將近一周,沒有刻意關閉後臺程序的內存分布狀態
  • 如何設置當單擊某個對象時運行指定的應用程式?
    在PowerPoint 2010中能否通過單擊某個對象來運行指定的應用程式?為指定對象設置動作效果使其可以控制應用程式的運行。設置單擊對象時運行應用程式步驟1        在幻燈片中的適當位置添加一個矩形,在其中輸入「打開記事本」,然後為其設置字體格式,字體為「方正姚體」,字號為「28」,並為其設置一種形狀樣式,名為「強烈效果-青綠,強調顏色1」,結果如圖8-30所示。
  • 全面解讀作業系統中的內存管理,你懂幾點?
    2.系統有多個進程同時在運行:如圖,理想情況下可以使進程A和進程B各佔物理內存的一邊,兩者互不幹擾,但這只是理想情況下,誰能確保程序沒有bug呢,進程B在後臺正常運行著,程式設計師在調試進程A時有可能就會誤操作到進程B正在使用的物理內存,導致進程B運行出現異常,兩個程序操作了同一地址空間,第一個程序在某一地址空間寫入某個值,第二個程序在同一地址又寫入了不同值,
  • 讓iPhone猝不及防,全球首款16G大運行內存安卓手機,要來了!
    眾所周知,電腦運行的快慢跟內存條容量有很大關係。同理,手機運行的快慢也和內存有很大關係。手機的系統內存又分為「手機運行內存」與「手機非運行內存」,前者相當於電腦的內存,後者就是手機的ROM和硬碟,簡稱機身內存。其中,手機運行內存越大,手機能運行多個程序且流暢,簡稱RAM。
  • 作業系統內存管理,你能回答這8個問題嗎?
    ,但這只是理想情況下,誰能確保程序沒有bug呢,進程B在後臺正常運行著,程式設計師在調試進程A時有可能就會誤操作到進程B正在使用的物理內存,導致進程B運行出現異常,兩個程序操作了同一地址空間,第一個程序在某一地址空間寫入某個值,第二個程序在同一地址又寫入了不同值,這就會導致程序運行出現問題,所以直接使用物理內存會使所有進程的安全性得不到保證。
  • 如何關閉手機中不必要的後臺運行軟體?
    我們先來說說蘋果手機的iOS系統,蘋果官方對於是否需要刪除iOS後臺程序給出過正面回答,用戶無需自行結束後臺運行的程序,程序一旦進入後臺就會進入休眠狀態。用戶刪除後臺程序,不僅會降低手機的運行效率(程序啟動勢必要慢於甦醒),依次還會造成電池壽命的損傷。既然如此,蘋果手機的用戶就放心使用,我們主要討論的對象是安卓手機用戶。