一點一滴探究 JVM 之記憶體結構

詩意偶然發表於2019-03-01

前言

我一直嘗試著用不一樣的文字來寫部落格!原因很簡單,你講的知識書上都有,那麼每個人為什麼不選擇看書而選擇看你的博文來學習呢?因為書上的內容都是大片大片描述性的文字,對於jvm這塊的知識,又是異常枯燥,但又不能不學習的硬骨頭!這恰好也就能說明Head First系列的書籍為什麼比較火的原因,每個人接收圖形知識的速度往往比文字性的東西要快很多。今後我也會嘗試用自己的特色來寫部落格,儘量能引起讀者的興趣,能從中學到東西,我就知足了!

今天的一點一滴探究JVM系列,打算複習一下jvm記憶體結構!至於學習這塊知識的好處?一,從面試的角度來看,你瞭解jvm,並且java基礎紮實,你才更有競爭力(因為我本人本科還沒畢業,所以考慮問題經常從面試者的角度來考慮)。其二,提高你對java的理解,知道你建立的每一個物件,每一個變數,都在什麼地方,如果不知道這些稀裡糊塗得寫程式碼,總會有一天會”翻車”的!好了,廢話不多說了,我們開始正題吧!

開始之前

Java與C++之間有一堵由記憶體動態分配和垃圾收集技術所圍成的”牆”, 牆外的人想進去,牆內的人想出來。
或許你經常看到StackOverFlowError, OutOfMemoryError無從下手,因為你壓根不知道,究竟是什麼東西造成記憶體爆了,當然,你也無法解決!

舉個簡單的例子

public class test {
    private int f() {
        f();
    }
    public static void main(String[] args) {
        f();
    }
}
複製程式碼

這個簡單的遞迴,不對,它不算是遞迴,因為沒有終止條件,但是你知道它最終會報什麼錯誤,知道為什麼會報這個錯誤嗎?究竟是那塊記憶體發生了錯誤?

這個問題,我們留在後面回答,是留在後面你自己解答,看完這篇博文,不用我說,這些問題你都會很清楚!相信我!

目標

你可能會好奇,你看完這篇文章你能學到什麼?

清楚你的物件會被分配在哪裡(不絕對)
理解哪些區域對執行緒來說是私有區,哪些區域是執行緒共享區域
知道方法呼叫發生了什麼?

等等等,你可能還會解釋你以前遇到一些匪夷所思的問題!總之,你如果之前沒了解過這些知識,那麼這些東西對你來說,就是成長!

牆內的世界

你可能很好奇,牆內究竟是什麼樣?接下來跟著我一探究竟

一點一滴探究 JVM 之記憶體結構

上圖就是jvm比較詳細的記憶體劃分,下面我們來按執行緒私有共享來劃分jvm記憶體區

一點一滴探究 JVM 之記憶體結構

下面我們來著重介紹一下這幾塊記憶體區域

程式計數器(Program Counter Register)

什麼是程式計數器呢,學過彙編的都知道,cs:ip組成的實體地址是下一條要執行的指令的地址,來吧!看圖

一點一滴探究 JVM 之記憶體結構

我們可以很清楚的看到,當前cs:ip指向的記憶體地址恰好就是我們要執行的下一條指令的位置,前面我們圖中(按執行緒私有共享劃分jvm記憶體的圖)又說了,程式計數器是執行緒私有的,再聯想一下我舉cs:ip的例子,我們可以很自然的想到,程式計數器其實就是記錄執行緒當前執行到了哪一條指令,因為什麼要記錄這個值呢?因為,如果我們有很多個執行緒,執行緒執行順序又是不可預料的,假如某一時刻我們在執行執行緒A裡面的指令,然後執行緒B又獲得了cpu的資源,去執行去執行緒B的指令,假如再過了一段時間之後,A又獲得了cpu的資源,想吃回頭草,此時回到執行緒A執行,它不知道要執行執行緒A的哪條指令!這是沒有程式計數器所形成的尷尬局面,但是有了執行緒私有的程式計數器,這個問題就不存在了,這就是程式計數器出現的原因,以及它的用處,我想你看完這段文字,應該已經對程式計數器這個概念完全理解了!

另外,我需要說明的一點是,程式計數器是Java虛擬機器規範中唯一一個沒有規定任何記憶體錯誤的區域!

虛擬機器棧(Vm Stack)

這塊區域是幹啥的?為啥也是執行緒私有的?

虛擬機器棧描述的是Java方法執行的記憶體模型
我們來解讀這句話,為什麼說Vm Stack是描述Java方法執行的記憶體模型呢?其實:

每個方法執行的時候都會建立一個棧幀(Stack Frame)的東西,學過c/c++的應該都對這個概念熟悉。棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口資訊等。每個方法從呼叫開始到結束的過程,都對應這Vm Stack中的入棧出棧的過程!這也就能回答開頭我們看到的那個問題了,很簡單錯誤在單執行緒情況下肯定是StackOverFlowError,多執行緒下OutOfMemoryError(上圖已經寫得很清楚了)

比如

public void test() {
    String name = "stormma";
    int age = 21;
}
JAVA架構群:678779467
複製程式碼

上面的例子的age變數和name引用都是儲存在虛擬機器棧的棧幀裡面的(因為我們前面說過了,一個方法從開始呼叫到結束呼叫的過程都對應著一個Vm Stack出棧入棧的過程)。

我們前面說了,這塊區域儲存了區域性變數表,運算元棧,動態連結,還有方法出口資訊等,我想你應該比較好奇這幾個概念。

區域性變數表: 區域性變數表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數,其中存放的資料的型別是編譯期可知的各種基本資料型別、物件引用(reference)和(returnAddress)型別(它指向了一條位元組碼指令的地址)。區域性變數表所需的記憶體空間在編譯期間完成計算的,即在Java程式被編譯成Class檔案時,就確定了所需分配的最大區域性變數表的容量。當進入一個方法時,這個方法需要在棧中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變數表的大小。

運算元棧: 運算元棧又常被稱為操作棧,運算元棧的最大深度也是在編譯的時候就確定了。32位資料型別所佔的棧容量為1, 64位資料型別所佔的棧容量為2。當一個方法開始執行時,它的操作棧是空的,在方法的執行過程中,會有各種位元組碼指令(比如:加操作、賦值元算等)向操作棧中寫入和提取內容,也就是入棧和出棧操作。Java虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。因此我們也稱Java虛擬機器是基於棧的,這點不同於Android虛擬機器,Android虛擬機器是基於暫存器的。基於棧的指令集最主要的優點是可移植性強,主要的缺點是執行速度相對會慢些;而由於暫存器由硬體直接提供,所以基於暫存器指令集最主要的優點是執行速度快,主要的缺點是可移植性差

動態連結: 每個棧幀都包含一個指向執行時常量池(在方法區中,後面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。Class檔案的常量池中存在有大量的符號引用,位元組碼中的方法呼叫指令就以常量池中指向方法的符號引用為引數。這些符號引用,一部分會在類載入階段或第一次使用的時候轉化為直接引用(如 final、static 域等),稱為靜態解析,另一部分將在每一次的執行期間轉化為直接引用,這部分稱為動態連線。

方法返回地址: 當一個方法被執行後,有兩種方式退出該方法:執行引擎遇到了任意一個方法返回的位元組碼指令或遇到了異常,並且該異常沒有在方法體內得到處理。無論採用何種退出方式,在方法退出之後,都需要返回到方法被呼叫的位置,程式才能繼續執行。方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,呼叫者的PC計數器的值就可以作為返回地址,棧幀中很可能儲存了這個計數器值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會儲存這部分資訊。方法退出的過程實際上等同於把當前棧幀出站,因此退出時可能執行的操作有:恢復上層方法的區域性變數表和運算元棧,如果有返回值,則把它壓入呼叫者棧幀的運算元棧中,調整PC計數器的值以指向方法呼叫指令後面的一條指令。
我想關於這個區域的東西我已經介紹完了,我想你也應該懂了。

下面我們來下一個區域: 堆(heap)

堆(Heap)

堆區,是一塊很有意思的區域,為啥有意思,因為這塊區域是所有執行緒共享的,也是我們大部分的物件的聚居地(為啥說是大部分呢?這個概念我們之後的文章會進行詳細的講解,如果你特別好奇,可以看一下我之前的文章, Java逃逸分析)!也是jvm管理的最大一塊記憶體(對了,上面的圖的大小不代表記憶體佔比,只是為了看著舒服而已)!也是gc開展工作的主要區域。

堆記憶體中分為一塊區域,用於儲存類資訊,靜態變數等等資料,這一塊區域之前叫做方法區後面又叫永久帶,之後改名叫做Meta-Area/Meta Space Area,後設資料空間,名字不重要,我們要清楚這塊區域是什麼作用就行了!

Meta-Area

這塊區域也是執行緒共享的區域,它主要儲存jvm載入類的類資訊,類變數,常量(這個在meta-area的常量區),即時編譯器編譯後的程式碼等資料。

執行時常量區

這個區域是Meta-Area的一部分,用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。這在我們的上一篇部落格有所涉及。

枯燥概念性的東西看完之後,我們來看一個例子,來加深一下這塊的印象:

public void test() {
    Object obj = new Object();
}
JAVA架構群:678779467
複製程式碼

對於這段程式碼會涉及Vm Stack、Java Heap、Meta-Area三個最重要的記憶體區域。

結合我們前面的例子,因為test()方法涉及到Vm Stack區,我想你應該明白,obj會存放在區域性變數表中,new Object(),我們前面說過我們大部分的物件都會儲存在Java Heap這個區域,所以,Java Heap儲存了這個例項物件!那麼你會很好奇,Meta-Area為啥會涉及到呢?

我們知道Meta-Area儲存了類的資訊,類變數常量等等東西!因為我們例項化Object對應的時候,要用到Object這個類的資訊,所以它會訪問Meta-Area的Object.class這個Class物件來獲得一些例項化物件需要的東西。

對了,作為補充,我想你還需要知道, obj引用怎麼你能訪問到Java Heap區的那個例項化物件

有兩種方式,一種使用過控制程式碼指標(學過c/c++對這些概念應該會很熟悉)

一點一滴探究 JVM 之記憶體結構

還有一種就是通過指標直接訪問

一點一滴探究 JVM 之記憶體結構

上圖來自深入理解JVM一書

本地方法棧(Native Method Stack)

這塊區域相對來說,沒有前面幾個概念重要。

該區域與虛擬機器棧所發揮的作用非常相似,只是虛擬機器棧為虛擬機器執行Java方法服務,而本地方法棧則為使用到的本地作業系統(Native)方法服務。

比如Java呼叫c/c++/彙編就用到這塊區域

結尾

我想你看完這篇博文,應該達到了我們文章開始之前的目標!這篇文章介紹的比較淺顯,本著用例子來解釋說明記憶體區域的作用,這樣我想你會更容易接收,總比大片的文字描述讓你更有興趣!如果你有什麼建議或者疑惑,可以通過GitHub聯絡我!

相關文章