Android進階(一):5分鐘帶你瞭解JVM的內存分配

前言

JVM的內存分配是一個老生常談的話題了,但是如果開發者想要開發出高質量的APP,那麼JVM的內存分配是必須要了解的。本文主要介紹JVM的內存分配。

JVM的內存區域劃分

在網上一些介紹JVM內存分配的文章中,他們將Java的內存大致分爲堆內存(heap)和棧內存(stack),這種劃分的方式,體現了開發者最關注的區域,但是並不完全準確。JVM會將內存劃分爲若干個不同的數據區域,主要分爲:程序計數器虛擬機棧本地方法棧方法區這五部分。

在介紹這五個內存區域前,我們先了解一下,Java文件被JVM加載到內存中的過程:

  1. A.java文件經過編譯器編譯,生成A.class字節碼文件
  2. 程序訪問A這個類時,會通過ClassLoader(類加載器)將A.class加載到JVM的內存中
  3. JVM將內存區域劃分爲若干個不同的數據區域,主要分爲:程序計數器虛擬機棧本地方法棧方法區這五部分。

JVM內存分佈

程序計數器

程序計數器的作用

Java程序是多線程的,線程的執行與掛起由CPU來決定。當CPU將一個線程(暫且就叫A線程)掛起,去執行另一線程(暫且叫B線程)時,這時需要記錄代碼已經執行到的位置;當CPU重新執行A線程時,就可以根據之前記錄的位置,知道自己應該從哪行代碼開始繼續執行下去。這就程序計數器的作用,程序計數器是虛擬機中一塊比較小的內存空間,主要用於記錄當前線程執行的位置。

當然,除了上述的線程的恢復線程操作之外,還有一些其他操作也依賴程序計數器來完成,比如:跳轉、異常處理等

程序計數器的幾個注意點:

  1. 程序計數器是線程私有的,每條線程內部都有一個私有的程序計數器。它的生命週期隨着線程創建而創建,隨着線程的結束而結束
  2. 在JVM規範中,對程序計數器這一區域沒有規定任何的OutOfMemoryError的情況
  3. 當一個線程正在執行一個Java方法的時候,程序計數器記錄的是正在執行的虛擬機字節碼指令的地址。如果正在執行的是Native方法,則計數器的值爲空

虛擬機棧

虛擬機棧也是線程私有的,與線程的生命週期同步。我們常聽說的 “JVM是基於棧的解釋器執行的,DVM是基於寄存器的解釋器執行的” 這句話中,“基於棧”就指的是虛擬機棧。每當有方法被執行時,JVM都會在虛擬機棧中創建一個棧幀。而這這個棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構。每當一個線程在執行某個方法時,都會爲這個方法創建一個棧幀(所以一個線程中可以有多個棧幀)。所以虛擬機棧其實就是用來描述Java方法執行的內存模型。
棧幀結構

棧幀的結構

這裏我們暫時就簡單瞭解一下棧幀的結構。

  • 局部變量表(Local Variable Table)
    局部變量表一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java文件編譯爲Class文件時,就在方法的Code屬性的max_locals數據項中確定了該方法所需要分配的局部變量表的最大容量。

  • 操作數棧(Operand Stack)
    操作數棧也常稱爲操作棧,它是一個後入先出棧。同局部變量表一樣,操作數棧的最大深度也在編譯的時候寫入到Code屬性的max_stacks數據項中。操作數棧的每一個元素可以是任意的Java數據類型,包括long和double。

    當一個方法剛剛開始執行的時候,這個方法的操作數棧是空的。在方法的執行過程中,會有各種字節碼指令往操作數棧中寫入和提取內容,也就是出棧/入棧操作。

  • 動態鏈接
    class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用作爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用,這種轉化稱爲靜態解析。另外一部分符號引用將在每一次運行期間轉化爲直接引用,這部分稱爲動態連接。

    虛擬機棧中,每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。

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

    • **正常退出:**指方法中的代碼正常完成,或者遇到任意一個方法返回的字節碼指令(如return)並退出,沒有拋出任何異常。
    • **異常退出:**指方法執行過程中遇到異常,並且這個異常在方法體內內部沒有得到處理,導致方法退出

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

虛擬機棧的幾個注意點:

在JVM規範中,對虛擬機棧規定了兩種異常狀況:

  1. **StackOverflowError:**當線程請求棧深超出JVM所允許的最大深度時拋出
  2. **OutOfMemoryError:**當JVM動態擴展到無法申請足夠內存時拋出

本地方法棧

本地方法棧和虛擬機棧基本相同,只不過是針對本地(Native)方法。有些虛擬機的實現中已經兩者合二爲一了(比如HotSpot)

堆(Heap)是JVM所管理的內存區域中最大的一塊,該區域唯一的目的就是存放對象實例,幾乎所有對象的實例都在堆裏分配,因此它也是Java垃圾收集器(GC)管理的主要區域。同時它也是所有線程共享的內存區域,因此被分配在區域的對象如果被多個線程訪問,需要考慮線程安全問題。

而按照對象存儲時間的不同,堆中的內存可以劃分爲新生代和年老代。其中新生代又被劃分爲Eden區和Survivor區。
堆內存分佈

方法區

方法區是JVM規範裏規定的一塊運行時數據區。方法區主要是存儲已經被JVM加載的類信息(版本、字段、方法、接口)、常量、靜態變量、即時編譯器編譯後的代碼與數據。方法區也是被各個線程共享的區域。

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