看完記得一鍵三連哦,微信搜索【沉默王二】關注這個沉默但有點東西的小丑,
今天的標題絕非標題黨,看下面這幅截圖就明白了,讀者真真的留言~

在談 JVM 記憶體區域劃分之前,我們先來看一下 Java 程式的具體執行程序,我畫了一幅圖,

Java 源代碼檔案經過編譯器編譯后生成位元組碼檔案,然后交給 JVM 的類加載器,加載完畢后,交給執行引擎執行,在整個執行的程序中,JVM 會用一塊空間來存盤程式執行期間需要用到的資料,這塊空間一般被稱為運行時資料區,也就是常說的 JVM 記憶體,
所以,當我們在談 JVM 記憶體區域劃分的時候,其實談的就是這塊空間——運行時資料區,
大家應該對官方出品的《Java 虛擬機規范》有所了解吧?了解這個規范可以讓我們更深入地理解 JVM,該規范主要包含 6 個部分,分別是:
- 第一章:引言
- 第二章:Java 虛擬機結構
- 第三章:Java 虛擬機編譯
- 第四章:Class 檔案
- 第五章:加載、鏈接和初始化
- 第六章:Java 虛擬機指令集
- 第七章:操作碼
根據第二章 Java 虛擬機結構中的規定,運行時資料區可以分為以下幾個部分,見下圖,

01、程式計數器
程式計數器(Program Counter Register)所占的記憶體空間不大,很小一塊,可以看作是當前執行緒所執行的位元組碼指令的行號指示器,位元組碼解釋器會在作業的時候改變這個計數器的值來選取下一條需要執行的位元組碼指令,像分支、回圈、跳轉、例外處理、執行緒恢復等功能都需要依賴這個計數器來完成,
在 JVM 中,多執行緒是通過執行緒輪流切換來獲得 CPU 執行時間的,因此,在任一具體時刻,一個 CPU 的內核只會執行一條執行緒中的指令,因此,為了執行緒切換后能恢復到正確的執行位置,每個執行緒都需要有一個獨立的程式計數器,并且不能互相干擾,否則就會影響到程式的正常執行次序,
也就是說,我們要求程式計數器是執行緒私有的,
《Java 虛擬機規范》中規定,如果執行緒執行的是非本地(native)方法,則程式計數器中保存的是當前需要執行的指令地址;如果執行緒執行的是本地方法,則程式計數器中的值是 undefined,
為什么本地方法在程式計數器中的值是 undefined 的?因為本地方法大多是通過 C/C++ 實作的,并未編譯成需要執行的位元組碼指令,
由于程式計數器中存盤的資料所占的空間不會隨程式的執行而發生大小上的改變,因此,程式計數器是不會發生記憶體溢位現象(OutOfMemory)的,
02、Java 虛擬機堆疊
Java 虛擬機堆疊中是一個個堆疊幀,每個堆疊幀對應一個被呼叫的方法,當執行緒執行一個方法時,會創建一個對應的堆疊幀,并將堆疊幀壓入堆疊中,當方法執行完畢后,將堆疊幀從堆疊中移除,堆疊遵循的是后進先出的原則,所以執行緒當前執行的方法對應的堆疊幀必定在 Java 虛擬機堆疊的頂部,
堆疊幀包含以下 5 個部分,見下圖,

1)區域變數表
顧名思義,就是用來存盤方法中的區域變數的,包括方法的引數,對于基本資料型別的變數,直接存盤變數的值;對于參考型別的變數,存盤的是物件的參考,區域變數表的大小在編譯期間就確定了,程式執行期間,它的大小是不會改變的,
2)運算元堆疊
運算式的計算是在運算元堆疊中完成的,當一個方法剛開始執行的時候,這個方法的運算元堆疊是空的,在方法的執行程序中,會有各種位元組碼指令往運算元堆疊中寫入和提取內容,也就是入堆疊/出堆疊操作,例如,在做算術運算的時候是通過運算元堆疊來進行的,又或者在呼叫其他方法的時候是通過運算元堆疊來進行引數傳遞的,
3)指向運行時常量池的參考
當前方法所屬的類的運行時常量池的參考,參考其他的常量類或者使用字串常量池中的字串,
4)方法回傳地址
方法執行完(不論是正常執行還是發生了例外)后需要回傳到方法被呼叫的位置,程式才能繼續執行,方法回傳地址保存一些用來幫助恢復上層方法的執行狀態的資訊,
5)動態鏈接
每個堆疊幀都包含了一個指向運行時常量池中該堆疊幀所屬方法的參考,持有這個參考是為了支持方法呼叫程序中的動態鏈接,
與程式計數器一樣,Java 虛擬機堆疊也是執行緒私有的,它的生命周期和執行緒相同,描述的是 Java 方法執行的記憶體模型,每次方法呼叫的資料都是通過堆疊傳遞的,
Java 虛擬機堆疊會出現兩種錯誤:
- StackOverFlowError:當執行緒請求堆疊的深度超過 Java 虛擬機堆疊的最大深度的時候拋出,
- OutOfMemoryError:如果 Java 虛擬機堆疊允許動態擴容,當堆疊擴容時無法申請到足夠的記憶體時拋出,
最有名的 HotSpot 虛擬機的堆疊容量是不允許動態擴容的,所以在 HotSpot 虛擬機上是不會出現 OutOfMemoryError 的,
03、本地方法堆疊
本地方法堆疊與 Java 虛擬機堆疊類似,區別是本地方法堆疊執行的是本地方法,也就是帶有 native 關鍵字修飾的方法,
在 HotSpot 虛擬機中,本地方法堆疊和 Java 虛擬機堆疊不做區分,
04、堆
堆是所有執行緒共享的一塊記憶體區域,在 Java 虛擬機啟動的時候創建,用來存盤物件(陣列也是一種物件),
以前,Java 中“幾乎”所有的物件都會在堆中分配,但隨著 JIT(Just-In-Time)編譯器的發展和逃逸技術的逐漸成熟,所有的物件都分配到堆上漸漸變得不那么“絕對”了,從 JDK 7 開始,Java 虛擬機已經默認開啟逃逸分析了,意味著如果某些方法中的物件參考沒有被回傳或者未被外面使用(也就是未逃逸出去),那么物件可以直接在堆疊上分配記憶體,
簡單解釋一下 JIT 和逃逸分析,
常見的編譯型語言如 C++,通常會把代碼直接編譯成 CPU 所能理解的機器碼來運行,而 Java 為了實作“一次編譯,處處運行”的特性,把編譯的程序分成兩部分,首先它會先由 javac 編譯成通用的中間形式——位元組碼,然后再由解釋器逐條將位元組碼解釋為機器碼來執行,所以在性能上,Java 可能會干不過 C++ 這類編譯型語言,
為了優化 Java 的性能 ,JVM 在解釋器之外引入了 JIT 編譯器:當程式運行時,解釋器首先發揮作用,代碼可以直接執行,隨著時間推移,即時編譯器逐漸發揮作用,把越來越多的代碼編譯優化成本地代碼,來獲取更高的執行效率,解釋器這時可以作為編譯運行的降級手段,在一些不可靠的編譯優化出現問題時,再切換回解釋執行,保證程式可以正常運行,
逃逸分析(Escape Analysis),簡單來講就是,Hotspot 虛擬機可以分析新創建物件的使用范圍,并決定是否在 Java 堆上分配記憶體的一項技術,
堆是 Java 垃圾收集器管理的主要區域,因此也被稱作 GC 堆(Garbage Collected Heap),從垃圾回收的角度來看,由于垃圾收集器基本都采用了分代垃圾收集的演算法,所以堆還可以細分為:新生代和老年代,新生代還可以細分為:Eden 空間、From Survivor、To Survivor 空間等,進一步劃分的目的是更好地回收記憶體,或者更快地分配記憶體,
堆這最容易出現的就是 OutOfMemoryError 錯誤,分為以下幾種表現形式:
OutOfMemoryError: GC Overhead Limit Exceeded:當 JVM 花太多時間執行垃圾回收并且只能回收很少的堆空間時,就會發生該錯誤,java.lang.OutOfMemoryError: Java heap space:假如在創建新的物件時, 堆記憶體中的空間不足以存放新創建的物件, 就會引發該錯誤,和本機的物理記憶體無關,和我們配置的虛擬機記憶體大小有關!
05、元空間
JDK 8 的時候,原有的方法區(更準確的說應該是永久代)被徹底移除,取而代之的是元空間,
我們來說說方法區吧,方法區和堆一樣,是執行緒共享的區域,它用來存盤已經被 Java 虛擬機加載的類資訊、常量、靜態變數,以及便器編譯后的代碼等,
在有些地方,方法區也被稱為永久代,但其實不能這么理解,
《Java 虛擬機規范》中只規定了有方法區這么一個概念和它的作用,并沒有規定如何去實作它,那么不同的 Java 虛擬機可能就會有不同的實作,永久代是 HotSpot 對方法區的一種實作形式,也就是說,永久代只是 HotSpot 中的一個概念,而方法區則是 Java 虛擬機規范中的一個定義,一種規范,
換句話說,方法區和永久代的關系就像是 Java 中介面和類的關系,類實作了介面,
在方法區中,還有一塊非常重要的部分,也就是運行時常量池,在講 class 檔案的時候,提到了每個 class 檔案都會有個常量池,用來存放字串常量、類和介面的名字、欄位名、常量等等,運行時常量池和 class 檔案的常量池是一一對應的,它就是通過 class 檔案中的常量池來構建的,
JDK 7 之前,運行時常量池中包含著字串常量池,都在方法區,
JDK 7 的時候,字串常量池從方法區中拿出來放到了堆中,運行時常量池中的其他東西還在方法區中,
JDK 8 的時候,HotSpot 移除了永久代,也就是說方法區不存在了,取而代之的是元空間,也就意味著字串常量池在堆中,運行時常量池跑到了元空間,
再來說說為什么要將永久代 (PermGen) 或者說方法區替換為元空間 (MetaSpace) ,
第一,永久代放在 Java 虛擬機中,就會受到 Java 虛擬機記憶體大小的限制,而元空間使用的是本地記憶體,也就脫離了 Java 虛擬機記憶體的限制,
第二,JDK 8 的時候,在 HotSpot 中融合了 JRockit 虛擬機,而 JRockit 中并沒有永久代的概念,因此新的 HotSpot 就沒有必要再開辟一塊空間來作為永久代了,
我最近花了近一周的時間整理了一份純 Java 版的刷題筆記,一共 300 道題解!
圖文并茂,截圖如下,不只是干巴巴的題解代碼,很多題都給出了多種解題思路,真的會提高大家刷題的幸福指數~

刷完 300 道 LeetCode 題后,我膨脹到要飄起來了!純正 Java 版
對于我們 Java 程式員來說,不需要像 C/C++ 程式員那樣時時刻刻關心著記憶體泄露和記憶體溢位的問題,但實際的作業中,這兩個問題出現的頻率還是蠻高的,尤其是在多執行緒并發的情況下,如果不了解 Java 虛擬機是如何管理記憶體的,那么一旦遇到問題可能就會束手無策,
了解 Java 虛擬機的記憶體區域劃分有助于我們更好的去理解 Java 虛擬機,從而掌握記憶體問題排查的主動權,
我是沉默王二,希望大家都能把編程學好,如果內容有幫助的話,請自覺做個手有余香的一鍵三連人,嘿嘿!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/276242.html
標籤:其他
上一篇:C語言進階之旅(4)
