本文已收錄 【修煉內功】躍遷之路
不論作技術仍是作業務,對於Java開發人員來說,理解JVM各類原理的重要性沒必要再多言java
對於C/C++而言,能夠輕易地操做任意地址的內存,而對於已申請內存數據的生命週期,又要擔負起維護的責任。不知各位在初學C語言時,是否經歷過因爲內存泄漏致使系統內存不足,又或者由於誤操做系統關鍵內存致使強制關機……segmentfault
對於Java使用者來講,內存由虛擬機直接管理,不容易出現內存泄漏或內存溢出等問題,將開發人員解放出來,使得更多的精力能夠用於具體實現上。也正是所以,一旦出現內存泄漏或溢出問題,若是不瞭解JVM的內存管理原理,那麼將會對問題的排查帶來極大的困難。數組
JVM在執行Java程序的過程當中,會將所管理的內存劃分爲不一樣的區域,這些區域各自都有本身的用途、可見性及生命週期,根據《Java虛擬機規範》的規定,JVM所管理的內存包含以下幾個區域jvm
程序計數器是一個很小的內存區域,不在RAM上,而是直接劃分在CPU上,用於JVM在解釋執行字節碼時,存儲當前線程執行的字節碼行號,每條線程都擁有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲函數
字節碼解釋器工做時,就是經過改變程序計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常等基礎功能都須要依賴計數器來完成測試
若是線程正在執行的是一個Java方法,則程序計數器記錄的是正在執行的虛擬機字節碼指令地址;若是執行的是native方法,則計數器的值爲空。此內存區是惟一一個在虛擬機規範中沒有規定任何OutOfMemoryError的區域優化
Java堆,是平常工做中最常接觸的、也是虛擬機所管理的最大的一塊內存區域,其被全部線程共享,在虛擬機啓動時建立,此區域惟一的目的就是存放對象實例ui
《深刻理解Java虛擬機》全部的對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展及逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在對上也逐漸變得不是那麼"絕對"了spa
從內存回收角度,Java堆分爲新生代和老年代,新生代又分爲E(den)空間和S(urvivor)0空間、S(urvivor)1空間操作系統
從內存分配角度,Java堆可能分爲多個線程私有的分配緩衝區
若是存在實例未完成堆內存分配,且堆沒法再擴展時(經過-Xmx及-Xms控制),將會拋出OutOfMemoryError異常
對於堆上各區域的分配、回收等細節,將在《[JVM] 虛擬機垃圾收集器》系列文章中詳述
只要不斷建立對象,而且保證GC Roots到對象之間有可達路徑來避免GC回收,那麼在對象數量達到堆的最大容量限制後就會產生內存溢出異常
/** * VM Args: -Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError * * @author manerfan */ public class HeapOOM { static class OOMObject { private int i; private long l; private double d; } public static void main(String[] args) { List<OOMObject> list = new LinkedList<>(); while (true) { list.add(new OOMObject()); } } }
指定堆大小固定爲5MB且不能擴展,運行結果
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid71020.hprof ... Heap dump file created [9186606 bytes in 0.069 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at HeapOOM.main(HeapOOM.java:19)
當Java堆內存溢出時,異常堆棧信息"java.lang.OutOfMemoryError"會跟着進一步提示"Java heap space"
對Dump出來的堆轉儲快照進行分析(如Eclipse Memory Analyzer),能夠確認內存中的對象是不是必要的,能夠清楚究竟是內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)
觀察堆使用狀況,以下圖
虛擬機棧也是線程私有的,它的生命週期與線程相同,每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息,方法執行時棧幀入棧,方法結束時棧幀出棧
局部變量表存放編譯器可知的各類基本數據類型、對象引用及returnAddress類型,局部變量表所需的內存空間在編譯期間肯定,運行期間不會再改變,具體的分析會在《[JVM] 虛擬機棧及字節碼基礎》中介紹
虛擬機棧規定了兩種異常:若是線程請求的棧深度大於虛擬機容許的最大棧深度,則會拋出StackOverflow異常;若是虛擬機能夠動態擴展棧深度,在擴展時沒法申請足夠內存,則會拋出OutOfMemoryError異常
可使用遞歸,無限增長棧的深度
/** * StackSOF * * @author Maner.Fan */ public class StackSOF { private int stackLen = 1; public void stackLeak() { stackLen++; stackLeak(); } public static void main(String[] args) { StackSOF stackSOF = new StackSOF(); try { stackSOF.stackLeak(); } catch (Throwable e) { System.out.println("statck length: " + stackSOF.stackLen); throw e; } } }
運行結果
statck length: 18455 Exception in thread "main" java.lang.StackOverflowError at StackSOF.stackLeak(StackSOF.java:13) at StackSOF.stackLeak(StackSOF.java:13) at StackSOF.stackLeak(StackSOF.java:13) at ...
對於棧空間的OutOfMemoryError,不管是減小最大堆容量、仍是減小最大棧容量、仍是增長局部變量大小、仍是無限建立線程,都沒有模擬出棧空間的OutOfMemoryError,卻是在堆空間比較小的時候會產生java.lang.OutOfMemoryError: Java heap space
堆異常
環境
java version "1.8.0_212" Java(TM) SE Runtime Environment (build 1.8.0_212-b10) Java HotSpot(TM) 64-Bit Server VM (build 25.212-b10, mixed mode) macOS Mojave 10.14.4 2.2GHz Intel Core i7 16GB 1600 MHZ DDR3
思路
/** * VM Args: -Xms20M -Xmx20M -Xss512K * * @author Maner.Fan */ public class StackOOM { private void dontStop() { long l0 = 0L; long l1 = 1L; long l2 = 2L; long l3 = 3L; long l4 = 4L; long l5 = 5L; long l6 = 6L; long l7 = 7L; long l8 = 8L; long l9 = 9L; long l10 = 10L; long l11 = 11L; long l12 = 12L; long l13 = 13L; long l14 = 14L; long l15 = 15L; long l16 = 16L; long l17 = 17L; long l18 = 18L; long l19 = 19L; while(true) {} } public void stackLeak() { while (true) { new Thread(() -> dontStop()).start(); } } public static void main(String[] args) { StackOOM stackOOM = new StackOOM(); stackOOM.stackLeak(); } }
本地方法棧與虛擬機棧的運行運行機制一致,用於存儲每一個Native方法的執行狀態,惟一區別在於虛擬機棧爲執行Java方法服務,而本地方法棧爲執行Native方法服務,不少虛擬機直接將本地方法棧與虛擬機棧合二爲一
同虛擬機棧同樣,本地方法棧也會拋出StackOverflow及OutOfMemoryError異常
在Java7及其以前,虛擬機中存在一塊內存區域叫方法區(Method Area),一樣爲線程共享,其主要用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,有時候會將該區域稱之爲永久代(Permanent Generation),但本質上二者並不等價
相對而言,GC行爲在這個區域是比較少出現的,但並不是數據進入了方法區就意味着"永久"存在,該區域的GC目標主要是針對常量池的回收及類型的卸載,但這個區域的回收成績比較難以使人滿意,尤爲是對類型的卸載
當方法區沒法知足內存分配需求時,將拋出OutofmemoryError異常
在Java7中,常量池已經從方法區移到了堆中,到了Java8及以後的版本,方法區已經被永久移除,取而代之的是元空間(Metaspace)
This is part of the JRockit and Hotspot convergence effort. JRockit customers do.
一方面,移除方法區是爲了和JRockit進行融合;另外一方面,方法區大小受到-XX: PermSize
和 -XX: MaxPermSize
兩個參數的限制,而這兩個參數又受到JVM設定的內存大小限制,這就致使在使用過程當中可能出現方法區內存溢出的問題
Metaspace並不在虛擬機內存中,而是使用本地內存,所以Metaspace具體大小理論上取決於系統的可用內存,一樣也能夠經過參數進行配置(-XX:MetaspaceSize
-XX:MaxMetaspaceSize
)
固然,Metaspace也是有OutOfMemoryError風險的,可是因爲Metaspace使用本機內存,所以只要不要代碼裏面犯過低級的錯誤,OOM的機率基本是不存在的
因爲Java8以後,方法區被永久移除,這裏咱們再也不測試方法區(永久代)的內存溢出
最簡單的模擬Metaspace內存溢出,咱們只須要無限生成類信息便可,類佔據的空間老是會超過Metaspace指定的空間大小的,這裏藉助Cglib來模擬類的不斷加載
/** * VM Args: -XX:MetaspaceSize=8M -XX:MaxMetaspaceSize=16M * * @author Maner.Fan */ public class MetaspaceOOM { public static void main(String[] args) throws InterruptedException { System.out.println("MetaspaceOOM.java"); while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback( (MethodInterceptor)(obj, method, args1, methodProxy) -> methodProxy.invokeSuper(obj, args1) ); enhancer.create(); } } static class OOMObject {} }
運行結果
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348) at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492) at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117) at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480) at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305) at MetaspaceOOM.main(MetaspaceOOM.java:19)
當Java元空間內存溢出時,異常堆棧信息"java.lang.OutOfMemoryError"會跟着進一步提示"Metaspace"
觀察元空間使用狀況,以下圖
直接內存並非虛擬機運行時數據區的一部分,最典型的示例即是NIO,其引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,使用Native函數庫直接分配堆外內存,經過一個存儲在隊中的DirectByteBuffer對象做爲這塊內存的引用進行操做
直接內存的分配不會受到Java堆大小的限制,但會受到本機總內存大小及尋址空間的限制,一旦本機內存不足以分配堆外內存時,一樣會拋出OutOfMemoryError異常
對象的建立是爲了使用,Java程序執行時須要經過棧上的reference數據來找到堆上的具體對象數據進行操做,目前主流的訪問方式有兩種:句柄訪問、直接指針訪問
Java堆中將分配一塊內存做爲句柄池,棧中的reference存儲對象實例句柄的地址
句柄包含兩個指針,一個指針記錄對象實例的內存地址,另外一個記錄對象類型數據的地址
使用句柄的方式訪問對象數據,須要進行兩次指針定位,但其優勢在於,在GC過程當中對象被移動時,只須要修改句柄中對象實例數據指針便可
棧中reference直接存儲堆中對象實例數據的內存地址,而對象類型數據的地址存放在對象實例數據中
使用直接指針訪問的好處在於訪問速度快,其只須要一次指針定位,但在GC過程當中對象被移動時,須要將全部指向該對象實例的reference值修改成移動後的內存地址
參考:
深刻理解Java虛擬機