一、JVM虛擬機數據區
·?虛擬機棧
? ? ? ? 1、? 線程私有
? ? ? ? 2、 每個方法被執行的時候都會創建一個棧幀用于存儲局部變量表,操作棧,動態鏈接,方法出口等信息。每一個方法被調用的過程就對應一個棧幀在虛擬機棧中從入棧到出棧的過程。
? ? ? ? 3、棧幀: 是用來存儲數據和部分過程結果的數據結構
? ? ? ? 4、?棧幀大小確定時間: 編譯期確定,不受運行期數據影響
? ? ? ? 5、虛擬機棧的生命周期和線程是相同
? ? ? ? 6、虛擬機棧是一個后入先出的棧。棧幀是保存在虛擬機棧中的,棧幀是用來存儲數據和存儲部分過程結果的數據結構,同時也被用來處理動態鏈接(Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)
? ? ? ? 7、線程運行過程中,只有一個棧幀是處于活躍狀態,稱為“當前活躍棧幀”,當前活動棧幀始終是虛擬機棧的棧頂元素:如圖->
? ? ? ? 8、棧幀細節如圖:
?
? ? ? ? 9、局部變量表是一組局部變量值存儲空間,用于存放方法參數和方法內部定義的局部變量。在Java文件編譯為Class文件時,就在方法表的Code屬性的max_locals數據項中確定了該方法需要分配的最大局部變量表的容量?
? ? ? ? 10、操作數棧也常被稱為操作棧,它是一個后入先出棧。JVM底層字節碼指令集是基于棧類型的,所有的操作碼都是對操作數棧上的數據進行操作,對于每一個方法的調用,JVM會建立一個操作數棧,以供計算使用。和局部變量一樣。操作數棧的最大深度也是編譯的時候寫入到方法表的code屬性的max_stacks數據項中。操作數棧的每一個元素可以是任意的Java數據類型,包括long、double。32位數據類型所占的棧容量為1,64位數據類型所占的棧容量為2。棧容量的單位為“字寬”,對于32位虛擬機來說,一個“字寬”占4個字節,64位虛擬機來說,一個“字寬”占8個字節。當一個方法剛剛執行的時候,這個方法的操作數棧是空的,在方法執行的過程中,會有各種字節碼指向操作數棧中寫入和提取值,也就是入棧與出棧操作
????????11、每個棧幀都包含一個指向運行時常量池中該棧幀所屬性方法的引用,持有這個引用是為了支持方法調用過程中的動態連接。在Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用為參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析。另外一部分將在每一次的運行期期間轉化為直接引用,這部分稱為動態連接
? ? ? ? 12、當一個方法被執行后,有兩種方式退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的的方法稱為調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法方式稱為正常完成出口(Normal Method Invocation Completion)。另外一種退出方式是,在方法執行過程中遇到了異常,并且這個異常沒有在方法體內得到處理,無論是Java虛擬機內部產生的異常,還是代碼中使用athrow字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種退出方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的調用都產生任何返回值的。 ? ? 無論采用何種方式退出,在方法退出之前,都需要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,調用者PC計數器的值就可以作為返回地址,棧幀中很可能會保存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會保存這部分信息。 方法退出的過程實際上等同于把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的局部變量表和操作數棧,把返回值(如果有的話)壓入調用都棧幀的操作數棧中,調用PC計數器的值以指向方法調用指令后面的一條指令等
·?本地方法棧
? ? ? ? 1、本地方法本質上時依賴于實現的,虛擬機實現的設計者們可以自由地決定使用怎樣的機制來讓Java程序調用本地方法
? ? ? ? 2、任何本地方法接口都會使用某種本地方法棧。當線程調用Java方法時,虛擬機會創建一個新的棧幀并壓入Java棧。然而當它調用的是本地方法時,虛擬機會保持Java棧不變,不再在線程的Java棧中壓入新的幀,虛擬機只是簡單地動態連接并直接調用指定的本地方法
? ? ? ? 3、很可能本地方法接口需要回調Java虛擬機中的Java方法,在這種情況下,該線程會保存本地方法棧的狀態并進入到另一個Java棧
? ? ? ? 4、如下圖展示了JAVA虛擬機內部線程運行的全景圖。一個線程可能在整個生命周期中都執行Java方法,操作它的Java棧;或者它可能毫無障礙地在Java棧和本地方法棧之間跳轉
· ?堆(劃重點)
? ? ? ? 1、堆的內存結構如圖:
? ? ? ? 2、堆內存分為年輕代(Young Generation)、老年代(Old Generation)
? ? ? ? 3、年輕代又分為Eden和Survivor區。Survivor區由FromSpace和ToSpace組成。Eden區占大容量,Survivor兩個區占小容量,默認比例是8:1:1
? ? ? ? 4、堆內存用途:存放的是對象,垃圾收集器就是收集這些對象,然后根據GC算法回收
? ? ? ? 5、JDK1.8版本廢棄了永久代,替代的是元空間(MetaSpace),元空間與永久代上類似,都是方法區的實現,他們最大區別是:元空間并不在JVM中,而是使用本地內存
? ? ? ? 6、元空間有注意有兩個參數:
MetaspaceSize :初始化元空間大小,控制發生GC閾值
MaxMetaspaceSize : 限制元空間大小上限,防止異常占用過多物理內存
? ? ? ? 7、移除永久代原因:為融合HotSpot JVM與JRockit VM(新JVM技術)而做出的改變,因為JRockit沒有永久代。
有了元空間就不再會出現永久代OOM問題了
? ? ? ? 8、各代關系:
新生成的對象首先放到年輕代Eden區,當Eden空間滿了,觸發Minor GC,存活下來的對象移動到Survivor0區,Survivor0區滿后觸發執行Minor GC,Survivor0區存活對象移動到Suvivor1區,這樣保證了一段時間內總有一個survivor區為空。經過多次Minor GC仍然存活的對象移動到老年代。
老年代存儲長期存活的對象,占滿時會觸發Major GC=Full GC,GC期間會停止所有線程等待GC完成,所以對響應要求高的應用盡量減少發生Major GC,避免響應超時。
Minor GC : 清理年輕代
Major GC : 清理老年代
Full GC : 清理整個堆空間,包括年輕代和永久代
所有GC都會停止應用所有線程
? ? ? ? 9、QA
為什么survivor分為兩塊相等大小的幸存空間?
主要為了解決碎片化。如果內存碎片化嚴重,也就是兩個對象占用不連續的內存,已有的連續內存不夠新對象存放,就會觸發GC
為什么會堆內存溢出?
在年輕代中經過GC后還存活的對象會被復制到老年代中。當老年代空間不足時,JVM會對老年代進行完全的垃圾回收(Full GC)。如果GC后,還是無法存放從Survivor區復制過來的對象,就會出現OOM(Out of Memory)
OOM常見原因分析:
老年代內存不足:java.lang.OutOfMemoryError:Javaheapspace
解決辦法:java 堆內存溢出, 此種情況最常見, 一般由于內存泄露或者堆的大小設置不當引起。對于內存泄露, 需要通過內存監控軟件查找程序中的泄露代碼, 而堆大小可以通過虛擬機參數 - Xms,-Xmx 等修改
永久代內存不足:java.lang.OutOfMemoryError:PermGenspac
解決辦法:java 永久代溢出, 即方法區溢出了, 一般出現于大量 Class 或者 jsp 頁面, 或者采用 cglib 等反射機制的情況, 因為上述情況會產生大量的 Class 信息存儲于方法區。當出現此種情況時可以通過更改方法區的大小來解決, 使用類似 - XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。注意, 過多的常量尤其是字符串也會導致方法區溢出
java.lang.StackOverflowError
? ?解決方法: JAVA 虛擬機棧溢出, 一般是由于程序中存在死循環或者深度遞歸調用造成的, 棧大小設置太小也會出現此種溢出。可以通過虛擬機參數 - Xss 來設置棧的大小
重點來了:
(1)、Java heap space報錯的原因及如何解救:
? ? ? ? (1.1)、請求創建一個超大對象,通常是一個大數組,舉例子:從文件或者db中讀取了100w條數據,申明了一個List或者Map,一條數據的大小哪怕1kb大小,這100w數據就要占用1Gb內存
? ? ? ? 這里給個樣例代碼,后面不會再上復現代碼了,各位自己下去動手去驗證:
package com.feixiang.platform.stu.jvm.oom.stack;import com.feixiang.platform.stu.jvm.reference.strong.User;import java.util.ArrayList;
import java.util.List;/*** Created with IntelliJ IDEA.* Description: 創造一個OOM來實驗:* 一定要夠大的數組對象* 運行參數一定要修改,不然無法如愿** @PackageName com.feixiang.platform.stu.jvm.oom.stack* @Author: ldwtxwhspring* @Date: 2023-12-02 下午6:01* @Version: 1.0.0*/
public class StackHeapOomExample {public static void main(String[] args) {List<User> userList = new ArrayList<>();for(int i=0;i<1000000;i++){User user = new User();user.setId(i);user.setName("feixiang_"+i);user.setAddress("xxxx省—xxx市-xxx路-xxx小區-xxx樓"+i+"號房間");user.setMobile("133xxxx5680");userList.add(user);}}
}
運行參數修改如圖:
?
運行結果如圖:
?
? ? ? ? (1.2)、超出預期的訪問量/數據量,通常是上游系統請求流量飆升,常見于各類促銷/秒殺活動,可以結合業務流量指標排查是否有尖狀峰值。例如:雙十一幾萬件商品快速被推向訂單池,連續操作有不斷鎖庫存加事務完整性邏輯,這些內存肯定不會被回收,而且會存在于完整的訂單周期內。
? ? ? ? (1.3)、過度使用(Finalizer),該對象沒有立即被 GC
? ? ? ? (1.4)、內存泄漏(Memory Leak),大量對象引用沒有釋放,JVM 無法對其自動回收,常見于使用了 File 等資源沒有回收,例如:讀寫文件完成沒有刷新流或者沒有關閉管道。
解決方法:
????????針對大部分情況,通常只需要通過 -Xmx 參數調高 JVM 堆內存空間即可。如果仍然沒有解決,可以參考以下情況做進一步處理:
????????
- 如果是超大對象,可以檢查其合理性,比如是否一次性查詢了數據庫全部結果,而沒有做結果數限制。
- 如果是業務峰值壓力,可以考慮添加機器資源,或者做限流降級。
- 如果是內存泄漏,需要找到持有的對象,修改代碼設計,比如關閉沒有釋放的連接。
- 對于外部網絡請求,可以設置超時時間,請求回調改造
(2)、PermGen space報錯原因分析及解救:
? ? ? ?(2.1)、 該錯誤表示永久代 (Permanent Generation) 已用滿, 通常是因為加載的 class 數目太多或體積太大
? ? ? ? (2.2)、原因分析,永久代存儲對象主要包括以下幾類:? ? ? ??
- 加載/緩存到內存中的 class 定義,包括類的名稱,字段,方法和字節碼;
- 常量池;
- 對象數組/類型數組所關聯的 class;
- JIT 編譯器優化后的 class 信息。
- PermGen 的使用量與加載到內存的 class 的數量/大小正相關。
? ? ? (2.2)、解決方案,根據 Permgen space 報錯的時機,可以采用不同的解決方案,如下所示:
- 程序啟動報錯,修改 -XX:MaxPermSize 啟動參數,調大永久代空間。
- 應用重新部署時報錯,很可能是沒有應用沒有重啟,導致加載了多份 class 信息,只需重啟 JVM 即可解決。
- 運行時報錯,應用程序可能會動態創建大量 class,而這些 class 的生命周期很短暫,但是 JVM 默認不會卸載 class,可以設置 -XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC這兩個參數允許 JVM 卸載 class。
- 如果上述方法無法解決,可以通過 jmap 命令 dump 內存對象 jmap-dump:format=b,file=dump.hprof ,然后利用 Eclipse MAT?Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation?功能逐一分析開銷最大的 classloader 和重復 class
(3)、java.lang.OutOfMemoryError:GC overhead limit exceeded 錯誤原因及解決:
當 Java 進程花費 98% 以上的時間執行 GC,但只恢復了不到 2% 的內存,且該動作連續重復了 5 次,就會拋出 java.lang.OutOfMemoryError:GC overhead limit exceeded 錯誤。簡單地說,就是應用程序已經基本耗盡了所有可用內存, GC 也無法回收。
此類問題的原因與解決方案跟 Javaheap space 非常類似,請回看上文
(4)、Metaspace錯誤原因及解救:
JDK 1.8 使用 Metaspace 替換了永久代(Permanent Generation),該錯誤表示 Metaspace 已被用滿,通常是因為加載的 class 數目太多或體積太大。
此類問題的原因與解決方法跟 Permgenspace 非常類似,可以參考上文。需要特別注意的是調整 Metaspace 空間大小的啟動參數為 -XX:MaxMetaspaceSize。
(5)、Unable to create new native thread錯誤原因及解救:
? ? ? ? (5.1)、每個 Java 線程都需要占用一定的內存空間,當 JVM 向底層操作系統請求創建一個新的 native 線程時,如果沒有足夠的資源分配就會報此類錯誤。
? ? ? ? (5.2)、原因分析,JVM 向 OS 請求創建 native 線程失敗,就會拋出 Unableto createnewnativethread,常見的原因包括以下幾類:
- 線程數超過操作系統最大線程數 ulimit 限制;
- 線程數超過 kernel.pid_max(只能重啟);
- native 內存不足;
? ? ? ? (5.3)、該問題發生的常見過程主要包括以下幾步:
- JVM 內部的應用程序請求創建一個新的 Java 線程;
- JVM native 方法代理了該次請求,并向操作系統請求創建一個 native 線程;
- 操作系統嘗試創建一個新的 native 線程,并為其分配內存;
- 如果操作系統的虛擬內存已耗盡,或是受到 32 位進程的地址空間限制,操作系統就會拒絕本次 native 內存分配;
- JVM 將拋出 java.lang.OutOfMemoryError:Unableto createnewnativethread 錯誤。
? ? ? ? (5.4)、解決方案
- 升級配置,為機器提供更多的內存;
- 降低 Java Heap Space 大小;
- 修復應用程序的線程泄漏問題;
- 限制線程池大小;
- 使用 -Xss 參數減少線程棧的大小;
- 調高 OS 層面的線程最大數:執行 ulimia-a 查看最大線程數限制,使用 ulimit-u xxx 調整最大線程數限制ulimit -a … 省略部分內容 … max user processes (-u) 16384
(6)、Out of swap space原因分析及解救:
? ? ? ? (6.1)、該錯誤表示所有可用的虛擬內存已被耗盡。虛擬內存(Virtual Memory)由物理內存(Physical Memory)和交換空間(Swap Space)兩部分組成。當運行時程序請求的虛擬內存溢出時就會報 Outof swap space? 錯誤。
? ? ? ? (6.2)、原因分析,該錯誤出現的常見原因包括以下幾類:
- 地址空間不足;
- 物理內存已耗光;
- 應用程序的本地內存泄漏(native leak),例如不斷申請本地內存,卻不釋放。
- 執行 jmap-histo:live 命令,強制執行 Full GC;如果幾次執行后內存明顯下降,則基本確認為 Direct ByteBuffer 問題。
? ? ? ? (6.3)、解決方案,根據錯誤原因可以采取如下解決方案:
- 升級地址空間為 64 bit;
- 使用 Arthas 檢查是否為 Inflater/Deflater 解壓縮問題,如果是,則顯式調用 end 方法。
- Direct ByteBuffer 問題可以通過啟動參數 -XX:MaxDirectMemorySize 調低閾值。
- 升級服務器配置/隔離部署,避免爭用。
(7)、Kill process or sacrifice child分析及解決:
? ? ? ? (7.1)、有一種內核作業(Kernel Job)名為 Out of Memory Killer,它會在可用內存極低的情況下“殺死”(kill)某些進程。OOM Killer 會對所有進程進行打分,然后將評分較低的進程“殺死”,具體的評分規則可以參考 Surviving the Linux OOM Killer。不同于其他的 OOM 錯誤, Killprocessorsacrifice child 錯誤不是由 JVM 層面觸發的,而是由操作系統層面觸發的。
? ? ? ? (7.2)、原因分析
默認情況下,Linux 內核允許進程申請的內存總量大于系統可用內存,通過這種“錯峰復用”的方式可以更有效的利用系統資源。
然而,這種方式也會無可避免地帶來一定的“超賣”風險。例如某些進程持續占用系統內存,然后導致其他進程沒有可用內存。此時,系統將自動激活 OOM Killer,尋找評分低的進程,并將其“殺死”,釋放內存資源。
? ? ? ? (7.3)、解決方案
- 升級服務器配置/隔離部署,避免爭用。
- OOM Killer 調優。
(8)、Direct buffer memory原因及解救:
? ? ? ? (8.1)、java 允許應用程序通過 Direct ByteBuffer 直接訪問堆外內存,許多高性能程序通過 Direct ByteBuffer 結合內存映射文件(Memory Mapped File)實現高速 IO。
? ? ? ? (8.2)、原因分析
- Direct ByteBuffer 的默認大小為 64 MB,一旦使用超出限制,就會拋出 Directbuffer memory 錯誤。
? ? ? ? (8.3)、解決方案
- Java 只能通過 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通過 Arthas 等在線診斷工具攔截該方法進行排查。
- 檢查是否直接或間接使用了 NIO,如 netty,jetty 等。
- 通過啟動參數 -XX:MaxDirectMemorySize 調整 Direct ByteBuffer 的上限值。
- 檢查 JVM 參數是否有 -XX:+DisableExplicitGC 選項,如果有就去掉,因為該參數會使 System.gc() 失效。
- 檢查堆外內存使用代碼,確認是否存在內存泄漏;或者通過反射調用 sun.misc.Cleaner 的 clean() 方法來主動釋放被 Direct ByteBuffer 持有的內存空間。
- 內存容量確實不足,升級配置。
·?程序計數器
? ? ? ? 1、線程私有
? ? ? ? 2、每個線程一塊內存,指向當前正在執行的字節碼的行號。如果當前線程是native方法,則其值為null
? ? ? ? 3、如果執行的是java方法,那么記錄的是正在執行的虛擬機字節碼指令的地址的地址,如果是native方法,計數器的值為空(undefined)
? ? ? ? 4、這塊內存區域是虛擬機規范中唯一沒有OutOfMemoryError的區域
二、本地內存
·?元數據區
1、主要用來保存被虛擬機加載的類信息、常量、靜態變量以及即時編譯器編譯后的代碼等數據
2、存在OOM的區:一個元數據區的大小決定了Java虛擬機可以裝載的類的多少
3、包括final修飾的變量、字面量、類和接口全限定名、字段、方法名稱以及修飾符等永恒不變的東西
4、并不是所有的字面量都會存儲在類文件常量池中,比如對于方法內(注意是方法)整數字面量,如果值在-32768~32767之間則會被直接嵌入JVM指令中去,不會保存在常量池中
·?直接內存
1、java通過native方法去調用
2、在調用這種native方法的時候,就會有線程對應的本地方法棧,這個其實類似于java虛擬機棧。也是存放各種native方法的局部變量表之類的信息
3、還有一塊區域,不是jvm的,通過NIO中的allocateDirect這種API,可以在jvm堆外分配內存空間,然后通過java虛擬機棧里的DirectByteBuffer來引用和操作堆外內存空間