摘要:也正是因此,一旦出現內存泄漏或溢出問題,如果不了解的內存管理原理,那么將會對問題的排查帶來極大的困難。
本文已收錄【修煉內功】躍遷之路
不論做技術還是做業務,對于Java開發人員來講,理解JVM各種原理的重要性不必再多言
對于C/C++而言,可以輕易地操作任意地址的內存,而對于已申請內存數據的生命周期,又要擔負起維護的責任。不知各位在初學C語言時,是否經歷過由于內存泄漏導致系統內存不足,又或者因為誤操作系統關鍵內存導致強制關機……
對于Java使用者來說,內存由虛擬機直接管理,不容易出現內存泄漏或內存溢出等問題,將開發人員解放出來,使得更多的精力可以用于具體實現上。也正是因此,一旦出現內存泄漏或溢出問題,如果不了解JVM的內存管理原理,那么將會對問題的排查帶來極大的困難。
JVM在執行Java程序的過程中,會將所管理的內存劃分為不同的區域,這些區域各自都有自己的用途、可見性及生命周期,根據《Java虛擬機規范》的規定,JVM所管理的內存包含如下幾個區域
0x00 程序計數器程序計數器是一個很小的內存區域,不在RAM上,而是直接劃分在CPU上,用于JVM在解釋執行字節碼時,存儲當前線程執行的字節碼行號,每條線程都擁有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲
字節碼解釋器工作時,就是通過改變程序計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常等基礎功能都需要依賴計數器來完成
如果線程正在執行的是一個Java方法,則程序計數器記錄的是正在執行的虛擬機字節碼指令地址;如果執行的是native方法,則計數器的值為空。此內存區是唯一一個在虛擬機規范中沒有規定任何OutOfMemoryError的區域
0x01 堆Java堆,是日常工作中最常接觸的、也是虛擬機所管理的最大的一塊內存區域,其被所有線程共享,在虛擬機啟動時創建,此區域唯一的目的就是存放對象實例
《深入理解Java虛擬機》所有的對象實例以及數組都要在堆上分配,但是隨著JIT編譯器的發展及逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在對上也逐漸變得不是那么"絕對"了
從內存回收角度,Java堆分為新生代和老年代,新生代又分為E(den)空間和S(urvivor)0空間、S(urvivor)1空間
從內存分配角度,Java堆可能分為多個線程私有的分配緩沖區
如果存在實例未完成堆內存分配,且堆無法再擴展時(通過-Xmx及-Xms控制),將會拋出OutOfMemoryError異常
對于堆上各區域的分配、回收等細節,將在《[JVM] 虛擬機垃圾收集器》系列文章中詳述
Java堆溢出只要不斷創建對象,并且保證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) { Listlist = 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)
觀察堆使用情況,如下圖
0x02 虛擬機棧虛擬機棧也是線程私有的,它的生命周期與線程相同,每個方法在執行的同時都會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,方法執行時棧幀入棧,方法結束時棧幀出棧
局部變量表存放編譯器可知的各種基本數據類型、對象引用及returnAddress類型,局部變量表所需的內存空間在編譯期間確定,運行期間不會再改變,具體的分析會在《[JVM] 虛擬機棧及字節碼基礎》中介紹
虛擬機棧規定了兩種異常:如果線程請求的棧深度大于虛擬機允許的最大棧深度,則會拋出StackOverflow異常;如果虛擬機可以動態擴展棧深度,在擴展時無法申請足夠內存,則會拋出OutOfMemoryError異常
Java棧溢出 StackOverflow可以使用遞歸,無限增加棧的深度
/** * 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
對于??臻g的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(); } }0x03 本地方法棧
本地方法棧與虛擬機棧的運行運行機制一致,用于存儲每個Native方法的執行狀態,唯一區別在于虛擬機棧為執行Java方法服務,而本地方法棧為執行Native方法服務,很多虛擬機直接將本地方法棧與虛擬機棧合二為一
同虛擬機棧一樣,本地方法棧也會拋出StackOverflow及OutOfMemoryError異常
0x04 方法區/元空間 Method Area在Java7及其之前,虛擬機中存在一塊內存區域叫方法區(Method Area),同樣為線程共享,其主要用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據,有時候會將該區域稱之為永久代(Permanent Generation),但本質上兩者并不等價
相對而言,GC行為在這個區域是比較少出現的,但并非數據進入了方法區就意味著"永久"存在,該區域的GC目標主要是針對常量池的回收及類型的卸載,但這個區域的回收成績比較難以令人滿意,尤其是對類型的卸載
當方法區無法滿足內存分配需求時,將拋出OutofmemoryError異常
在Java7中,常量池已經從方法區移到了堆中,到了Java8及之后的版本,方法區已經被永久移除,取而代之的是元空間(Metaspace)
為什么要移除Method AreaThis is part of the JRockit and Hotspot convergence effort. JRockit customers do.
一方面,移除方法區是為了和JRockit進行融合;另一方面,方法區大小受到-XX: PermSize和 -XX: MaxPermSize兩個參數的限制,而這兩個參數又受到JVM設定的內存大小限制,這就導致在使用過程中可能出現方法區內存溢出的問題
MetaspaceMetaspace并不在虛擬機內存中,而是使用本地內存,因此Metaspace具體大小理論上取決于系統的可用內存,同樣也可以通過參數進行配置(-XX:MetaspaceSize -XX:MaxMetaspaceSize)
當然,Metaspace也是有OutOfMemoryError風險的,但是由于Metaspace使用本機內存,因此只要不要代碼里面犯太低級的錯誤,OOM的概率基本是不存在的
Java元空間溢出由于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"
觀察元空間使用情況,如下圖
0x05 直接內存直接內存并不是虛擬機運行時數據區的一部分,最典型的示例便是NIO,其引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,使用Native函數庫直接分配堆外內存,通過一個存儲在隊中的DirectByteBuffer對象作為這塊內存的引用進行操作
直接內存的分配不會受到Java堆大小的限制,但會受到本機總內存大小及尋址空間的限制,一旦本機內存不足以分配堆外內存時,同樣會拋出OutOfMemoryError異常
0x06 對象的訪問定位對象的創建是為了使用,Java程序執行時需要通過棧上的reference數據來找到堆上的具體對象數據進行操作,目前主流的訪問方式有兩種:句柄訪問、直接指針訪問
句柄訪問Java堆中將分配一塊內存作為句柄池,棧中的reference存儲對象實例句柄的地址
句柄包含兩個指針,一個指針記錄對象實例的內存地址,另一個記錄對象類型數據的地址
使用句柄的方式訪問對象數據,需要進行兩次指針定位,但其優點在于,在GC過程中對象被移動時,只需要修改句柄中對象實例數據指針即可
直接指針訪問棧中reference直接存儲堆中對象實例數據的內存地址,而對象類型數據的地址存放在對象實例數據中
使用直接指針訪問的好處在于訪問速度快,其只需要一次指針定位,但在GC過程中對象被移動時,需要將所有指向該對象實例的reference值修改為移動后的內存地址
參考:
深入理解Java虛擬機
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/74545.html
摘要:本文已收錄修煉內功躍遷之路在淺談虛擬機內存模型一文中有簡單介紹過,虛擬機棧是線程私有的,每個方法在執行的同時都會創建一個棧幀,方法執行時棧幀入棧,方法結束時棧幀出棧,虛擬機中棧幀的入棧順序就是方法的調用順序寫了很多文字,但都不盡如意,十分慚 本文已收錄【修煉內功】躍遷之路 showImg(https://segmentfault.com/img/bVbtSi5?w=1654&h=96...
摘要:本文已收錄修煉內功躍遷之路在誕生之初便提出,各提供商發布很多不同平臺的虛擬機,這些虛擬機都可以載入并執行同平臺無關的字節碼。設計者在第一版虛擬機規范中便承諾,時至今日,商業機構和開源機構已在之外發展出一大批可以在上運行的語言,如等。 本文已收錄【修煉內功】躍遷之路 Java在誕生之初便提出 Write Once, Run Anywhere,各提供商發布很多不同平臺的虛擬機,這些虛擬機...
摘要:本文已收錄修煉內功躍遷之路學習語言的時候,需要在不同的目標操作系統上或者使用交叉編譯環境,使用正確的指令集編譯成對應操作系統可運行的執行文件,才可以在相應的系統上運行,如果使用操作系統差異性的庫或者接口,還需要針對不同的系統做不同的處理宏的 本文已收錄【修煉內功】躍遷之路 showImg(https://segmentfault.com/img/bVbtpPd?w=2065&h=11...
摘要:本文已收錄修煉內功躍遷之路我們寫的方法在被編譯為文件后是如何被虛擬機執行的對于重寫或者重載的方法,是在編譯階段就確定具體方法的么如果不是,虛擬機在運行時又是如何確定具體方法的方法調用不等于方法執行,一切方法調用在文件中都只是常量池中的符號引 本文已收錄【修煉內功】躍遷之路 showImg(https://segmentfault.com/img/bVbuesq?w=2114&h=12...
摘要:本文已收錄修煉內功躍遷之路初次接觸的時候感覺表達式很神奇表達式帶來的編程新思路,但又總感覺它就是匿名類或者內部類的語法糖而已,只是語法上更為簡潔罷了,如同以下的代碼匿名類內部類編譯后會產生三個文件雖然從使用效果來看,與匿名類或者內部類有相 本文已收錄【修煉內功】躍遷之路 showImg(https://segmentfault.com/img/bVbui4o?w=800&h=600)...
閱讀 3626·2021-11-24 10:22
閱讀 3695·2021-11-22 09:34
閱讀 2498·2021-11-15 11:39
閱讀 1536·2021-10-14 09:42
閱讀 3669·2021-10-08 10:04
閱讀 1564·2019-08-30 15:52
閱讀 854·2019-08-30 13:49
閱讀 3025·2019-08-30 11:21