Java程序中經常用到的內存模塊到底有哪些?
前言
我們寫代碼時,通常會關注代碼與對象之間的流轉。但實際上,我們有沒有認真去關注過 java 程序運行時,類、對象、局部變量與方法調用鏈是存放在哪里的呢?
JVM 內存
眾所周知,java 程序是執行在 jvm 虛擬機中的,那在眾多的 jvm 虛擬機中,HotSpot VM 是當下最熱門的或者說使用得最多的 jvm 版本。在 HotSpot VM 中,整個虛擬機內存被劃分為幾大模塊:
- 堆(Heap)
- 方法區(Method Area)
- 程序計數器(Program Counter Register)
- 虛擬機棧(JVM Stacks)
- 本地方法棧(Native Method Stacks)

堆(Heap)
該區域就是我們平時正常 new 出來的對象待的地方,該區域也是我們平時接觸得最多的區域,這里細分為:Young Generation(新生代) 和 Old Generation(老生代) ,也就是我們平時經常關注的 YGC 和 FGC 的地方,而 Young Generation 中間又再細分為三個區域:Eden、From Survivor、To Survivor,YGC 會有各種的算法來對這三塊區域做針對性的垃圾回收算法,這里就先不展開討論,有興趣的可以參考 面試官,不要再問我“JAVA GC垃圾回收機制”了 。
該區域是所有線程所共享的,而存放的又是主要的業務對象,所以空間相對來說會是比較大的。一般可以使用 -Xms設置堆的最小空間大小 和 -Xmx設置堆的最大空間大小。

方法區(Method Area)
方法區和剛說的棧區域有很多相似的地方:線程共享、內存不連續、可擴展、可垃圾回收,同樣當無法再擴展時會拋出OutOfMemoryError異常。而方法區通常也被稱為非堆區域(non-heap),注意這要與堆外內存區分開!
方法區它存儲的是已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。就是一些與運行時生成的對象區別開,是一個固定存放物資的區域。
方法區的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是回收確實是有必要的。

程序計數器(Program Counter Register)
程序計數器的作用可以看做是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時就是通過改變計數器的值來選取下一條字節碼指令。其中,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴計數器來完成。
Java虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內核)只會執行一條線程中的指令,必須要有一個地方儲存每個線程具體處理到哪一步了,該區域就是做這件事了。

虛擬機棧(JVM Stacks)
學習過匯編語言的同學也許會比較容易理解它的作用。它是一個記錄線程調用方法模型的棧,每一條線程私有一個虛擬機棧,所以它也與對應線程生死與共。
其每個方法模型被稱為棧幀,棧幀會存放 4 個屬性:局部變量表、操作棧、動態鏈接、方法返回地址。

ps:對于其中的局部變量,如果是變量為基礎類型,棧幀會直接存儲對應的值,但如果是高級類型的話只會存放值的引用。
本地方法棧(Native Method Stacks)
與虛擬機棧作用相似,也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
區別在于虛擬機棧為虛擬機執行Java方法(字節碼)服務,而本地方法棧是為虛擬機使用到的Native方法服務。
對于 jvm 的內存,我們可以用這樣的一張圖來歸納起來以幫助對比理解。

堆外內存(Off-Heap Memory)
區別于 JVM 內存,java 常用的還有堆外內存。雖然 JVM 內存擁有非常完善的垃圾管理機制,可以讓開發人員無需關注內存資源回收隨心所欲地進行開發,但也正因為存在一連串非常復雜精妙的垃圾管理算法,導致在高并發情況(特別是寫內存多)下可能會出現頻繁的 YGC 和 FGC (每次 GC 都會引起程序卡頓),反而會成為性能瓶頸的幫兇!
針對這一類的業務情況,我們通常會使用堆外內存來提升我們的性能水平。堆外內存其實就是游離于 JVM 管理之外的物理機內存。不屬于 JVM 的管理,自然就沒有了 JVM 的那套 GC 算法,這樣能使我們有更加好的擴展性和 IO 速度。
在JAVA中,可以通過Unsafe和NIO包下的ByteBuffer來操作堆外內存,也可以使用第三方堆外緩存管理包例如 ohc(off-heap-cache) 來操作。