JVM記憶體模型,你看這一篇就夠了

語言: CN / TW / HK
摘要:JVM是一種用於計算裝置的規範,是一個虛構出來的計算機,通過在實際的計算機上模擬模擬各種計算機功能來實現的。

本文分享自華為雲社群《[雲駐共創]JVM記憶體模型的探知之旅》,作者:多米諾的古牌。

1. JVM介紹

1.1 什麼是JVM?

JVM是Java Virtual Machine(Java虛擬機器)的簡稱,是一種用於計算裝置的規範,是一個虛構出來的計算機,通過在實際的計算機上模擬模擬各種計算機功能來實現的。

1.2 JVM的優點

1.2.1 一次編寫,到處執行。

JVM可以讓java程式,一次編寫,匯出執行。讓底層程式碼和執行環境分離開,編寫好一份程式碼後,不用再次修改內容,只用通過安裝不同的JVM環境自動進行轉換即可執行,在各種系統中無縫連線。

1.2.2 自動記憶體管理,垃圾回收機制。

在Java誕生之時,C和C++稱霸天下,但是這兩種語言中沒有記憶體管理機制,是通過手動操作來進行的管理,非常麻煩和繁瑣。

此時Java應運而生,為了處理記憶體管理這個方面,專門設計了垃圾回收機制,來自動進行記憶體的管理。極大的優化了操作,讓程式設計師們不用正在噼裡啪啦在碼代的海洋中遨遊時,還要操心記憶體會不會溢位這些“影響我方輸出”的問題,頓時獲得了成噸的好評。

1.2.3 陣列下標越界檢查

在Java誕生之時,還有個讓當時C和C++大佬頭疼的問題是,陣列下標越界是沒有檢查機制的,這還了得,又是一個影響“我方暴力輸出”的罪魁禍首,因此JVM繼續抱著暖男的思想,又來了個愛的抱抱。

JVM又一次看見了大佬們的煩惱,果斷提供了陣列下標越界的自動檢查機制,在檢測到陣列下標出現越界後,會在執行時自動丟擲“java.lang.ArrayIndexOutOfBoundsException”這個異常,在當時可是感動了很多業界大佬(我猜的)。

1.2.4 多型

JVM還有一個多型功能,是通過相同介面,不同的例項進行實現,完成不同的業務操作,比如:定義了一個動物介面(裡面有一個吃的方法),我們就可以通過這個動物創造小貓(吃魚),再創造一個狗狗(吃肉),再創造一個小助手(吃零食,O(∩_∩)O哈哈~)。

仔細想想,對我們有啥影響呢,那好處老多了,比如:

(1)消除型別之間的耦合關係;

(2)可替換性;

(3)可擴充性;

(4)介面性;

(5)靈活性;

(6)簡化性;

1.3 JVM、JRE、JDK之間的關係

1.3.1 JVM的簡介

JVM是Java Virtual Machine的簡稱,是Java虛擬機器,是一種模擬出來的虛擬計算機,它通過在不同的計算機環境當中模擬實現計算功能來實現的。

引入Java虛擬機器後,Java語言在不同平臺上執行時就不需要重新編譯。在其中,Java虛擬機器遮蔽了與具體平臺的相關資訊,使得Java源程式在編譯完成之後即可在不同的平臺執行,達到“一次編譯,到處執行”的目的,Java語言重要的特點之一跨平臺,也即與平臺的無關性,其關鍵點就是JVM。

1.3.2 JRE的簡介

JRE是Java Runtime Environment的簡稱,是Java執行環境,是讓作業系統執行Java應用程式的環境,其內部包含JVM,也就是說JRE只負責對已經存在的Java源程式進行執行的操作,它不包含開發工具JDK,對JDK內部的編譯器、偵錯程式和其它工具均不包含。

1.3.3 JDK的簡介

JDK是Java Development Kit的簡稱,是Java開發工具包,是整個Java程式開發的核心。其主要包含了JRE、Java的系統類庫以及對Java程式進行編譯以及執行的工具,例如:javac.exe和java.exe命令工具等。

1.4 JVM的常見實現

Oracle(Hotspot、Jrockit)、BEA(LiquidVM)、IBM(J9)、taobaoVM(淘寶專用,對Hotspot進行了深度定製)、zing(垃圾回收機制非常快,到達1毫秒左右)。

1.5 JVM的記憶體結構圖

當Java程式編譯完成為.class檔案==》類載入器(ClassLoader)==》將位元組碼檔案載入進JVM中;

1.5.1方法區、堆

方法區中儲存的主要是類的資訊(類的屬性、成員變數、建構函式等)、堆(建立的物件)。

1.5.2虛擬機器棧、程式計數器、本地方法棧

堆中的物件呼叫方法時,方法會執行在虛擬機器棧、程式計數器、本地方法棧中。

1.5.3執行引擎

執行方法中程式碼時,程式碼通過執行引擎執行中的“直譯器”執行;方法中經常呼叫的程式碼,即熱點程式碼,通過“即時編譯器”執行,其執行速度非常快。

1.5.4 GC(垃圾回收機制)

GC是針對堆記憶體中沒有引用的物件進行回收,可以手動也可以自動。

1.5.5本地方法介面

因為JVM不能直接呼叫作業系統的功能,只能通過本地方法介面來呼叫作業系統的功能。

2. JVM記憶體結構-程式計數器

2.1 程式計數器的定義

Program Counter Register即程式計數器(暫存器),用於記錄下一條Jvm指令的執行地址。

2.2 操作步驟

javap主要用於操作JVM,javap -c 是對java程式碼進行反彙編操作。下圖為通過先編譯demo.java後,再執行javap -c demo的輸出結果:

其中第一列為二進位制位元組碼,即JVM指令,第二列為java原始碼。第一列中的序號為JVM指令的執行地址。

JVM會通過程式計數器記錄下一條需要執行的JVM指令的地址(比如第一行的0),然後交給直譯器解析為機器碼,最後交給cpu(只能識別機器碼),完成一行的執行。

想要執行下一行,繼續讓JVM的程式計數器記錄下一條地址(比如第二行的3),再交給直譯器解析後給cpu,以此類推執行結束。

2.3 特點

2.3.1 執行緒私有的

2.3.2 不會存在記憶體溢位

3. JVM記憶體結構-虛擬機器棧

3.1 定義

虛擬機器棧是每個執行緒執行所需要的記憶體空間,每個棧中由多個棧幀組成,每個執行緒中只能有一個活動棧幀(對應當前正在執行的方法),所有棧幀都遵循後進先出,先進後出的原則。

棧幀是每次呼叫方法時所佔用的記憶體,在棧幀中儲存的內容引數、區域性變數、返回地址。

注1:垃圾回收不涉及棧記憶體,因為棧記憶體是由方法呼叫產生的,當方法呼叫結束後會彈出棧。

注2:棧記憶體不是分配的越大越好,因為實體記憶體是一定的,棧記憶體越大,可以支援更多的遞迴呼叫,但是可執行的執行緒數會越來越少。

注3:方法的區域性變數,當其沒有逃離方法的作用範圍時,是執行緒安全的;如果其引用了物件(比如靜態變數,即共享變數,用物件作為引數的方法,返回值為物件的方法),並且逃離出了方法的作用範圍,就需要考慮執行緒安全的問題了。

3.2 棧記憶體溢位

3.2.1 發生原因

(1)虛擬機器棧中,棧幀過多(無限遞迴),如圖1棧幀過多;

(2)每個棧幀所佔用過大,如圖2 棧幀過大。

3.2.2 棧記憶體溢位小實驗

3.2.2.1 棧幀過多的小實驗

無限遞迴呼叫(棧幀過多)的小實驗,method1()方法在主方法中無限呼叫自己,那麼會發生什麼情況呢?

答案很明顯,程式崩潰了,產生了棧記憶體溢位錯誤,如下圖所示:

-Xss:該引數規定了每個執行緒虛擬機器棧的大小;

接著我們通過設定一個虛擬機器棧的大小是256k試試會發生什麼?

我們發現當我們調整了虛擬機器棧的大小後執行了4315次方法後記憶體就溢位了,而調整虛擬機器棧之前,我們是23268次,很明顯我們可以通過-Xss引數調整虛擬機器棧的大小來控制記憶體的溢位情況。

3.2.2.2 執行緒執行診斷小實驗

想象中的場景,大佬在瘋狂輸出,突然CPU爆表了,顯示CPU佔用過多,如何去定位哪行程式碼的問題,是的是哪行(大佬都很忙的好嗎,當然要精確了,一分鐘幾千萬上下的,O(∩_∩)O哈哈~)?

Linux環境下:

在後臺執行Stack_6這個java位元組碼(.class)檔案:

注:無論是否將nohup命令的輸出重定向到終端,輸出都將附加到當前目錄的 nohup.out 檔案中。

(1)通過top命令,檢視程序(相當於工作管理員),發現了一個佔用CPU達到100%的可疑傢伙,這還了得,趕緊瞅瞅具體發生了什麼,還有沒有王法,這讓其他小夥伴還怎麼愉快的玩耍,秒速糾錯ING。。。

top命令,檢視哪個程序佔用CPU過高,返回程序號。

(2) 通過ps H -eo pid,tid ,%cpu | grep 命令過濾工作管理員中的內容。

注:ps H -eo pid,tid,%cpu |grep,是通過ps命令檢視哪個執行緒佔用CPU過高,返回程序id,其中pid為程序id,tid為執行緒id,%cpu為CPU佔用情況;

發現了罪魁禍首,這一串串心驚肉跳的紅色。。。

(3) 通過jstack 程序id檢視,20389這個有問題的程序中具體的情況。

注:jstack 程序id,是通過jstack 命令定位具體哪段程式碼出現佔用CPU過高,注意jstack命令查詢的執行緒id是16進位制的,需要轉換;

發現裡面有一堆執行的程式碼,那麼我們怎麼找到具體是哪個傢伙搞事情的呢?上圖我們可以發現搞事情的執行緒是20441,那麼我們通過計算器將20441轉換為16進位制的4FD9再去試試,真相只有一個。

通過對比nid(執行緒id)4fd9,我們發現這個叫thread1的執行緒一直在執行(RUNNABLE狀態),並且檢視到位置是位於Stack_6.java檔案的第11行出現的問題。。。

現在我們回到原始碼中,在Stack_6檔案的第11行,我們發現原來這裡一直在執行死迴圈,終於找到你,還好我沒放棄,奈斯~

4. JVM記憶體結構-本地方法棧

4.1 定義

由於Java本身有時候是無法直接和作業系統底層互動的,但有時候需要Java呼叫本地的C或C++方法,所以此時本地方法棧應運而生,它們是一類帶有native關鍵字的方法。

5. JVM記憶體結構-堆

5.1 定義

Heap堆:是通過new關鍵字建立的物件存放在堆中

5.2 特點

5.2.1執行緒共享

堆中存放的物件都是執行緒共享的,因此都是需要考慮執行緒安全問題的。

5.2.2有垃圾回收機制

因為堆中存放的物件存放了大量的物件,因此給他配了個小助手——垃圾回收機制(可以調自動擋和手動擋哦~)。

5.3 堆記憶體溢位小實驗

5.3.1 修改堆記憶體大小引數的小實驗

繼續幻想一個場景,當一個大佬開發完一個段程式碼的時候(當然一般大佬都是很自信的,我寫的程式碼怎麼可能有問題,不存在的。。。),但是測試可跑不了,穩妥起見咱們還是默默得搞測試試試嘛,安全第一。但是機器的記憶體就這麼大,大佬肯定跑了很多次了,都沒出現問題的,這不是找茬嘛。。。還是默默改下機子引數再試試吧(想去懟大佬,一定要拿出證據嘛~)。

-Xmx:JVM調優引數之一,該引數表示java堆可以擴充套件到的最大值。下面上案例:

在執行了26次之後,果斷的後臺報了堆記憶體溢位錯誤。

下面通過-Xmx JVM調優引數將堆記憶體調小至8m,再試試會發生什麼呢?

操作基本和棧記憶體溢位的時候的案例一樣,次數明顯變小了,只調用了17次就出現了堆記憶體溢位錯誤了。

5.3.2 堆記憶體診斷的小實驗

jps工具:檢視當前系統中有哪些java程序

jmap工具:檢視堆記憶體的佔用情況jmap -heap 程序id

jconsole工具:圖形化的工具,擁有多功能的監測功能,可以連續監測。

下面我們通過執行程式碼後通過jconsole視覺化圖形工具,來檢視堆記憶體的使用情況。

上圖我們可以看到,在我們建立10mb的陣列物件時,記憶體使用有一定上升;然後在我們手動呼叫垃圾回收機制後,記憶體又得到了很大的釋放。

6. JVM記憶體結構-方法區

6.1 定義

Java虛擬機器中有一個被所有jvm執行緒共享的方法區。方法區有點類似於傳統程式語言中的編譯程式碼塊或者作業系統層面的程式碼段。它儲存著每個類的構造資訊,譬如執行時的常量池,欄位,方法資料,以及方法和構造方法的程式碼,包括一些在類和例項初始化和介面初始化時候使用的特殊方法。

方法區有個別稱non-heap(非堆),可以看作是一塊獨立於堆的記憶體空間,是JVM規範中定義的一個概念,用於儲存類資訊、常量池、靜態變數,JIT編譯後的程式碼等資料,具體放在哪裡,不同的實現可以放在不同的地方。

6.2 特點

(1)方法區與java堆一樣,是各個執行緒共享的記憶體區域;

(2)方法區在JVM啟動的時候被建立;

(3)方法區的大小,跟堆空間一樣,可以選擇固定大小或擴充套件;

(4)方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器同樣會丟擲溢位錯誤OutOfMemoryError;

(5)關閉JVM就會釋放這個區域的記憶體。

6.3 JVM記憶體結構示意圖

在JVM記憶體結構1.6的時候,方法區儲存在記憶體結構中,叫做永久代,裡面儲存了執行時的常量池(包含串池StringTable)、類的資訊、類載入器;

在JVM記憶體結構1.8的時候,方法區做為一個概念,儲存在本地記憶體中,叫做元空間,裡面儲存了執行時的常量池、類的資訊、類載入器,此時串池(StringTable)儲存在堆之中。

 

點選關注,第一時間瞭解華為雲新鮮技術~