前言
那些使用過 C 或者 C++ 的讀者一定會發現這兩門語言的內存管理機制與 Java 的不同。在使用 C 或者 C++ 編程時,程序員需要手動的去管理和維護內存,就是說需要手動的清除那些不需要的對象,否則就會出現內存泄漏與內存溢出的問題。
如果你使用 Java 語言去開發,你就會發現大多數情況下你不用去關心無用對象的回收與內存的管理,因為這一切 JVM 虛擬機已經幫我們做好了。了解 JVM 內存的各個區域將有助于我們深入了解它的管理機制,避免出現內存相關的問題和高效的解決問題。下面來講講面試中也是Java學習進階中必備的JVM知識,后續還會更新完JVM系列,觀看的朋友可以轉發關注下!
引出問題
在 Java 編程時我們會用到許多不同類型的數據,比如臨時變量、靜態變量、對象、方法、類等等。 那么他們的存儲方式有什么不同嗎?或者說他們存在哪?

運行時數據區域
Java 虛擬機在執行 Java 程序過程中會把它所管理的內存分為若干個不同的數據區域,各自有各自的用途。

1.程序計數器
線程私有的,可以看作是當前線程所執行字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。分支、循環、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
這時唯一一個沒有規定任何 OOM 異常的區域。
2.虛擬機棧
虛擬機棧也是線程私有的,生命周期與線程相同。棧里面存儲的是方法的局部變量、對象的引用等等。
在這片區域中,規定了兩種異常情況,當線程請求的棧深度大于虛擬機所允許的深度,將拋出 StackOverflowError 異常。當虛擬機棧動態擴展無法申請到足夠的內存時會拋出 OOM 異常。
3.本地方法棧
和虛擬機棧的作用相同,只不過它是為 Native 方法服務。HotSpot 虛擬機直接將虛擬機棧和本地方法棧合二為一了。
4.堆
堆是 Java 虛擬機所管理內存中最大的一塊。是所有線程共享的一塊內存區域,在虛擬機啟動時創建。這個區域唯一的作用就是存放對象實例,也就是 NEW 出來的對象。這個區域也是 Java 垃圾收集器的主要作用區域。
當堆的大小再也無法擴展時,將會拋出 OOM 異常。
5.方法區
方法區也是線程共享的內存區域,用于存儲已經被虛擬機加載的類信息、常量、靜態變量等等。當方法區無法滿足內存分配需求時,會拋出 OOM 異常。這個區域也被稱為永久代。
補充
雖然上面的圖里沒有運行時常量池和直接內存,但是這兩部分也是我們開發時經常接觸的。所以給大家補充出來。
運行時常量池
運行時常量池是方法區的一部分,Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。也會拋出 OOM 異常。
直接內存
直接內存并不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規范中定義的內存區域,但是卻是NIO 操作時會直接使用的一塊內存,雖然不受虛擬機參數限制,但是還是會受到本機總內存的限制,會拋出 OOM 異常。
JAVA8 的改變
對于方法區,它是線程共享的,主要用于存儲類的信息,常量池,方法數據,方法代碼等。我們稱這個區域為永久代。
大部分程序員應該都見過 java.lang.OutOfMemoryError:PermGen space 異常,這里的 PermGen space 其實指的就是方法區。由于方法區主要存儲類的相關信息,所以對于動態生成類的情況比較容易出現永久代的內存溢出,典型的場景是在 JSP 頁面比較多的情況,容易出現永久代內存溢出。
在JDK 1.8中,HotSpot 虛擬機已經沒有 PermGen space 這個區域了,取而代之的是一個叫做Metaspace (元空間)的東西。

變化就是移除了方法區,增加了元空間,與方法區最大的區別是:元空間不再虛擬機中,而是使用本地內存。默認情況下,元空間的大小僅受本地內存限制。
這樣更改的好處:
- 字符串常量存在方法區中,容易出現性能問題和內存溢出。
- 類和方法的信息等比較難確定大小,因此對于方法區大小的指定比較困難,太小容易出現方法區溢出,太大容易導致堆的空間不足。
- 方法區的垃圾回收會帶來不必要的復雜度,并且回收效率偏低(垃圾回收會在下一章給大家介紹)。
內存溢出
雖然有 JVM 幫我們管理內存,但是在實際開發過程中一定還會遇到內存溢出的問題。堆,棧,方法區都有可能出現內存溢出問題。下面我們就結合幾個實際的小例子來給大家展示一下,方便大家以后根據不同的情況對內存溢出問題進行快速準確的定位。
java.lang.OutOfMemoryError: Java heap space ———>java 堆內存溢出,此種情況最常見,一般由于內存泄露或者堆的大小設置不當引起。對于內存泄露,需要通過內存監控軟件查找程序中的泄露代碼,而堆大小可以通過虛擬機參數 -Xms、 -Xmx 等修改。
例子:在集合中無限加入對象,效果受到機器配置影響,可以主動更改堆大小方便演示。

java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法區溢出了,一般出現于大量Class 或者 JSP 頁面,或者采用 CGLIB 等反射機制的情況,因為上述情況會產生大量的 Class 信息存儲于方法區。此種情況可以通過更改方法區的大小來解決,使用類似 -XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。另外,過多的常量尤其是字符串也會導致方法區溢出,因為常量池也是方法區的一部分。
例子:無限加載 Class,需要在 JDK 1.8 之前的版本運行,因為1.8將方法區改成了元空間,利用了機器的內存,最好手動設置 -XX:MaxPermSize,將值調小一點。

java.lang.StackOverflowError ------> 不會拋 OOM error,但也是比較常見的 Java 內存溢出。Java 虛擬機棧溢出,一般是由于程序中存在死循環或者深度遞歸調用造成的,棧大小設置太小就會出現此種溢出。可以通過虛擬機參數 -Xss 來設置棧的大小。
例子:無法快速收斂的遞歸。

總結
JVM內存區域劃分,便于它能夠更加高效的管理自身的內存。當程序中出現這種由于JVM造成的內存溢出的情況的時候,需要根據不同的情況做不同的分析與處理。
最后
讀到這的朋友可以轉發關注下,后續還會更新JVM及性能調優系列的精選文章,謝謝您的支持!