Java程序員進階之路——一點一滴探究 JVM 之內存結構

前言

我一直嘗試着用不一樣的文字來寫博客!原因很簡單,你講的知識書上都有,那麼每個人爲什麼不選擇看書而選擇看你的博文來學習呢?因爲書上的內容都是大片大片描述性的文字,對於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內存區

下面我們來着重介紹一下這幾塊內存區域

程序計數器(Program Counter Register)

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

Java程序員進階之路——一點一滴探究 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;
}

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

我們前面說了,這塊區域存儲了局部變量表,操作數棧,動態鏈接,還有方法出口信息等,我想你應該比較好奇這幾個概念。

  • 局部變量表: 局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量,其中存放的數據的類型是編譯期可知的各種基本數據類型、對象引用(reference)和(returnAddress)類型(它指向了一條字節碼指令的地址)。局部變量表所需的內存空間在編譯期間完成計算的,即在Java程序被編譯成Class文件時,就確定了所需分配的最大局部變量表的容量。當進入一個方法時,這個方法需要在棧中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
  • 操作數棧: 操作數棧又常被稱爲操作棧,操作數棧的最大深度也是在編譯的時候就確定了。32位數據類型所佔的棧容量爲1, 64位數據類型所佔的棧容量爲2。當一個方法開始執行時,它的操作棧是空的,在方法的執行過程中,會有各種字節碼指令(比如:加操作、賦值元算等)向操作棧中寫入和提取內容,也就是入棧和出棧操作。Java虛擬機的解釋執行引擎稱爲“基於棧的執行引擎”,其中所指的“棧”就是操作數棧。因此我們也稱Java虛擬機是基於棧的,這點不同於Android虛擬機,Android虛擬機是基於寄存器的。基於棧的指令集最主要的優點是可移植性強,主要的缺點是執行速度相對會慢些;而由於寄存器由硬件直接提供,所以基於寄存器指令集最主要的優點是執行速度快,主要的缺點是可移植性差
  • 動態鏈接: 每個棧幀都包含一個指向運行時常量池(在方法區中,後面介紹)中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。Class文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用,一部分會在類加載階段或第一次使用的時候轉化爲直接引用(如 final、static 域等),稱爲靜態解析,另一部分將在每一次的運行期間轉化爲直接引用,這部分稱爲動態連接。
  • 方法返回地址: 當一個方法被執行後,有兩種方式退出該方法:執行引擎遇到了任意一個方法返回的字節碼指令或遇到了異常,並且該異常沒有在方法體內得到處理。無論採用何種退出方式,在方法退出之後,都需要返回到方法被調用的位置,程序才能繼續執行。方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,調用者的PC計數器的值就可以作爲返回地址,棧幀中很可能保存了這個計數器值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會保存這部分信息。方法退出的過程實際上等同於把當前棧幀出站,因此退出時可能執行的操作有:恢復上層方法的局部變量表和操作數棧,如果有返回值,則把它壓入調用者棧幀的操作數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令。

堆(Heap)

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

堆內存中分爲一塊區域,用於存儲類信息,靜態變量等等數據,這一塊區域之前叫做方法區後面又叫永久帶,之後改名叫做Meta-Area/Meta Space Area,元數據空間,名字不重要,我們要清楚這塊區域是什麼作用就行了!

Meta-Area

這塊區域也是線程共享的區域,它主要存儲jvm加載類的類信息,類變量,常量(這個在meta-area的常量區),即時編譯器編譯後的代碼等數據。

運行時常量區

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

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

public void test() {
 Object obj = new Object();
}

對於這段代碼會涉及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一書

Java程序員進階之路——一點一滴探究 JVM 之內存結構

本地方法棧(Native Method Stack)

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

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

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

結尾

我想你看完這篇博文,應該達到了我們文章開始之前的目標!這篇文章介紹的比較淺顯,本着用例子來解釋說明內存區域的作用,這樣我想你會更容易接收,總比大片的文字描述讓你更有興趣!另外,如果你想閱覽有關java的書籍,我這裏有的,都可以免費提供給你。


歡迎加入合作羣:805685193 免費獲取Dubbo、Redis、設計模式、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術視頻資料,完整架構思維導圖,和BATJ面試題及答案。

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