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