Java高級進階 1 深入JVM之JVM內存模型分析

    深入理解JVM內存模型和掌握處理JVM內存問題已經是java程序員必不可少基礎技能之一。

JVM內存模型  

 java內存區域主要分爲線程私有區域(程序技術器、java虛擬機棧、本地方法棧),線程共享區域(方法區、實例堆(java堆))和直接內存。

    線程私有區域生命週期與線程相同,依賴用戶線程的創建/銷燬。

    線程共享區域隨jvm的啓動/關閉而創建。

    直接內存不屬於JVM運行時數據區的一部分,但是也會頻繁使用。NIO提供了基於Channel和Buffer的IO方式,可以使用Native函數庫直接操作堆外內存,然後使用DirectByteBuffer對象作爲這塊內存的引用進行操作。

    程序計數器(線程私有)

   較小一塊內存區域,當前線程所執行字節碼的行號指示器,每個線程都有一個獨立的計數器。正在執行java方法,計數器記錄的是jvm當前執行指令的地址,如果是執行Native,則計數器爲空。該區域是jvm唯一沒有任何OutOfMemeryError情況的區域。

    虛擬機棧(線程私有)

    描述java方法執行的內存模型,生命週期與線程相同。每個方式在執行同時會創建一個棧幀,用來存儲局部變量表、操作數棧、動態鏈接、方法出口等。每個方法從調用開始到執行完成都對應一個棧幀的入棧和出棧。

    棧幀是用來存儲數據和部分結果的數據結構,同時也用來處理動態鏈接、方法返回值和異常分派等。棧幀隨着方法調用而創建,方法結束而銷燬。

    本地方法棧(線程私有)

    和虛擬機棧類似,區別是虛擬機棧是爲java方法服務,而本地方法棧是爲Native方法服務。

    堆(線程共享)

    虛擬機所管理的內存中最大的一塊,唯一目的就是存放對象實例,幾乎所有對象實例都是在堆分配內存。也是垃圾回收的主要區域。現代JVM採用分帶收集法,因此java堆從Gc的角度可以分爲(新生代(Egen區、From Survivior 和To Survivor)和老年代)。

    方法區(線程共享)

    是我們常說的永久代,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯的代碼等數據。

    運行時常量池

    運行時常量池也屬於方法區一部分,Class文件除了類版本、字段、方法、接口等信息外,還有一項就是運行時常量池,用於存放編譯期生成的字面量和符號引用。

   內存中對象分配、佈局和訪問

    對象創建

    1 類加載檢查

    檢查是否在常量池中定位到類的符號引用,檢查類是否已被加載、解析和初始化過。如果沒有加載需要先執行類加載過程。

    2 對象內存分配

    類加載檢查通過後,虛擬機將爲對象分配內存,類加載完後便可以確認對象所需要內存大小。

   3 內存空間初始化爲零值

   4 對象初始化

    對象內存佈局

    對象頭

對象頭包含兩部分信息,一是用於存儲對象自身運行時數據(哈希碼、GC分代年齡、鎖標誌、線程持有鎖、偏向線程ID、偏向時間戳等);另一部分是對象類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確認對象是那個類的實例。

    實例數據

    存儲對象有效信息,即代碼中所定義的各種類型字段內容

    對齊填充

    對齊填充不是必要的,沒特別含義,僅起到佔位的作用。

   對象訪問

    java程序通過虛擬機棧上的reference(對象引用)數據來操作堆上的對象實例,對象訪問方法取決於虛擬機的實現,目前主流對象訪問方法有兩種:通過句柄訪問對象和通過直接指針訪問對象。

句柄訪問對象:

 直接指針訪問對象:

句柄方式訪問的好處就是referencec存儲的是穩定的句柄地址,對象被移動只會改變句柄中的實例數據指針,reference本身不改變。直接指針方式獲取的好處就是速度更快,節省了一次指針定位的開銷。

    JVM內存異常分析     

    常見內存分析工具

  • jvisualvm.exe

    JDK自帶監控工具,可以對運行中的java應用程序進行全方面的監控分析,配合相應插件可以組成功能強大實用的性能分析工具

  • jconsole.exe

    JDK自帶監控工具,功能和visualvm類似,支持插件較少

  • jca

    是類工具,啓動命令java -jar jca433.jar,專業的線程分析工具,兼容sun/oracle JDK 線程堆dump

  • MAT

    Memory Analyzer Tool ,基於eclipse的內存溢出分析工具,專業的內存泄漏分析工具。使用方法

  • jprofile

    商業的java分析工具,功能強大,在內存泄漏分析和線程死鎖分析方面非常專業

JVM內存參數配置

    可以通過參數-Xmx(堆最大容量,默認物理內存1/4)、-Xms(堆初始容量 ,默認物理內存1/64)來指定堆的大小,如:-Xms128m -Xmx1003m。默認空餘堆內存小於40%,JVM會增大堆直到-Xmx最大限制,空餘堆內存大於70%,虛擬機會縮小堆直到-Xms初始值限制,可以通過設置-Xmx和-Xms值相等避免堆自動調整大小。

    -Xmn 通常爲-Xmx 的1/3或1/4

    -XX:NewRatio =2 新生代和老年代比例  ,-XX:NewRatio =2表示新生代佔堆空間1/3,老年代佔堆空間(2/3)

    -XX:NewSize 設置新生代最小空間大小

     -XX:MaxNewSize 設置新生代最大空間大小

    -XX:SurvivorRatio=8 新生代Eden區比例,默認爲8

    -XX:PrintGCDetails 打印Gc信息

    -XX:+HeapDumpOnOutOfMemoryError 設置虛擬機出現內存溢出時Dump出當前內存堆轉儲快照

    -XX:PermSize 設置永久代最小空間

    -XX:MaxPermSize 設置永久代最大空間

    -Xss參數設置棧內存容量(每個線程的棧大小)

    -XX:+UseParallelGC 選擇垃圾收集器爲並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用串行收集。

    -XX:ParallelGCThreads=20:配置並行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。

    -XX:MaxDirectMemorySize 設置直接內存大小,默認物理內存1/4

    JVM內存限制(最大)

    操作系統分配給每個進程的內存是有限的,譬如32位windows限制爲2GB,即2GB - Xmx - MaxPermSize剩下的空間被虛擬機棧和本地方法棧瓜分(程序計數器器可以忽略)。

     堆內存分配

    根據Gc的角度可以分爲(新生代(Eden區、From Survivior 和To Survivor)和老年代),其中java堆空間=新生代(1/3) + 老年代(2/3),新生代空間=Eden區(8/10) + From Survivor(1/10)+ To Survivor(1/10)。

   

    Java堆溢出

    當堆內存不足時,會拋出OutOfMemoryError,異常堆棧信息“java.lang.OutOfMemoryError:Java heap space”

    基本解決思路:1 通過內存映像分析工具(如Eclipse Memory Analyzer Tool)對堆轉儲快照Dump進行分析,2 定位內存OOM異常是內存泄漏還是內存溢出。3 如果是內存泄漏,可以進一步通過工具查看泄漏對象GC Roots的引用鏈,找到泄漏對象通過怎樣的路徑與GC Roots相關聯導致垃圾收集器無法自動回收,根據對象類型和引用鏈定位內存泄漏的代碼。4 如果不是內存泄漏,內存對象必須存在,那就需要考慮增加虛擬機堆參數(-Xmx和-Xms),從物理內存對比看是否可以調大,從程序代碼上檢查是否存在某些對象生命週期過長,嘗試調整程序設計減少程序運行期的內存消耗。

    oom溢出堆棧信息

    

[Full GC (Allocation Failure) [PSYoungGen: 7912K->7905K(9216K)] [ParOldGen: 7967K->7967K(10240K)] 15880K->15872K(19456K), [Metaspace: 2731K->2731K(1056768K)], 0.1014711 secs] [Times: user=0.75 sys=0.01, real=0.10 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid11364.hprof ...
Heap dump file created [27994219 bytes in 0.119 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.ArrayList.grow(Unknown Source)

     虛擬機棧和本地方法棧溢出

    java棧溢出在虛擬機規範中定義了兩類:StackOverflowError 線程請求的棧深度大於虛擬機允許的最大深度和OutOfMemoryError 擴展棧時無法申請打足夠內存。

    StackOverflowError 線程請求的棧深度大於虛擬機允許的最大深度,該問題常見於遞歸場景中,當遞歸深度過深或局部變量過多時,可能出現該問題。通常使用默認的虛擬機棧參數配置,棧的深度足夠滿足正常的方法調用(包括遞歸)。

    OutOfMemoryError 通常是創建過多線程導致的內存溢出,主要優化手段包括:優化程序設計避免過多創建線程,在不能減少線程數或更換64位操作系統情況下,只能通過-Xss參數降低每個線程棧內存容量來換取更多的線程。

    方法區和運行時常量池溢出

    方法區用於存放Class的相關信息,如類名、訪問修飾、常量池、字段描述等,該區域溢出主要出現在動態生成Class的場景,比如:大量JSP或動態JSP文件的應用、基於OSGi的應用、大量使用反射和動態代理、CGLib等Bytecode框架等。

    方法區溢出堆棧信息:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.ArrayList.grow(Unknown Source)

    本機直接內存溢出

    本機直接內存不屬於java運行時數據庫,該區域內存分配經常被忽略。該區域溢出主要出現在使用NIO的場景。直接內存溢出不會在Heap Dump文件中看見明顯異常,如果出現溢出且dump文件很小,可以考慮從直接內存溢出放心分析。

 

    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章