天天看點

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

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

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

Java與C++之間有一堵由記憶體動态配置設定和垃圾收集技術所圍成的”牆”, 牆外的人想進去,牆内的人想出來。

或許你經常看到StackOverFlowError, OutOfMemoryError無從下手,因為你壓根不知道,究竟是什麼東西造成記憶體爆了,當然,你也無法解決!

舉個簡單的例子

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

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

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

清楚你的對象會被配置設定在哪裡(不絕對)

了解哪些區域對線程來說是私有區,哪些區域是線程共享區域

知道方法調用發生了什麼?

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

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

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

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

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

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

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

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

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

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

這塊區域是幹啥的?為啥也是線程私有的?

虛拟機棧描述的是Java方法執行的記憶體模型

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

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

比如

上面的例子的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的一部分,用于存放編譯器生成的各種字面量和符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中。這在我們的上一篇部落格有所涉及。

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

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

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

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

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

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

我有一個微信公衆号,經常會分享一些Java技術相關的幹貨。如果你喜歡我的分享,可以用微信搜尋“Java團長”或者“javatuanzhang”關注。