目錄
類實例化加載順序
類的實例化順序
JVM創建對象的過程
JVM的運行機制
直接內存(Direct Memory)
JVM后臺運行的線程
JVM 常用參數
標準參數中比較有用的:
非標準參數又稱為擴展參數,比較有用的是
非Stable參數
class初始化過程是什么?
JVM內存模型如何分配的?
JVM的類加載階段 JVM結構
JVM性能調優的原則有哪些?
什么情況下需要JVM調優?
在JVM調優時,你關注哪些指標?
JVM常用參數有哪些?
JVM常用性能調優工具有哪些?
線上排查問題的一般流程是怎么樣的?
什么情況下,會拋出OOM呢?
系統OOM之前都有哪些現象?
如何進行堆Dump文件分析?
如何進行GC日志分析?
線上死鎖是如何排查的?
線上YGC耗時過長優化方案有哪些?
線上頻繁FullGC優化方案有哪些?
內存逃逸分析
如何進行線上堆外內存泄漏的分析?(Netty尤其居多)
線上元空間內存泄露優化方案有哪些?
java類加載器有哪些?
雙親委派機制是什么?
雙親委派機制
如何破壞雙親委派模型
如果基礎類又要調用用戶的代碼,
GC如何判斷對象可以被回收?
那么GcRoot有哪些?
回收機制是
如何回收內存對象,有哪些回收算法?
jvm有哪些垃圾回收器,實際中如何選擇?
JVM8為什么要增加元空間?
JVM8中元空間有哪些特點?
如何解決線上gc頻繁的問題?
內存溢出的原因有哪些,如何排查線上問題?
類實例化加載順序
-
加載:當程序訪問某個類時,JVM會首先檢查該類是否已經加載到內存中。如果尚未加載,則會進行加載操作。加載操作將類的字節碼文件加載到內存,并為其創建一個Class對象。
-
連接(驗證、準備、解析):
-
驗證:驗證階段會對類的字節碼進行驗證,確保它的結構正確且滿足Java虛擬機規范。
-
準備:準備階段會為類的靜態變量分配內存,并設置初始值,這些變量會在類初始化時賦予其真正的初始值。
-
解析:解析階段會將符號引用轉換為直接引用,建立虛擬機內部的數據結構,以便后續的訪問。
-
-
初始化:初始化階段將執行類的初始化代碼,包括靜態變量的賦值和靜態代碼塊的執行。類的初始化是按需進行的,即在首次使用該類或創建該類的實例時進行。
對于類的實例化順序,可以按照以下規則進行:
-
靜態變量和靜態代碼塊按照在類中的順序依次執行,且僅執行一次。
-
實例變量和實例代碼塊按照在類中的順序依次執行,在每次創建實例時都會執行一次。
-
構造方法最后執行,用于初始化實例的狀態。
類的實例化順序
JVM創建對象的過程
-
類加載:當程序首次使用某個類時,JVM會先進行類的加載。類加載是將類的字節碼文件加載到內存,并創建一個對應的Class對象。
-
分配內存:在類加載完成后,JVM會根據類的內部結構在堆內存中分配對象所需的內存空間。分配的方式可以是指針碰撞(Bump the Pointer)或空閑列表(Free List)。
-
初始化零值:JVM會將分配的內存空間初始化為零值,包括基本類型的零值和引用類型的null。
-
設置對象頭:JVM會設置對象的頭部信息,包括存儲對象的哈希碼、鎖狀態、GC標記等。
-
執行構造函數:JVM會執行類的構造函數,進行對象的初始化。構造函數負責對對象的屬性進行初始化,并可以執行其他必要的操作。
-
對象創建完成:經過以上步驟,JVM成功創建了一個對象,并將對象的引用返回給程序。
JVM的運行機制
JVM(Java Virtual Machine)是用于運行Java字節碼的虛擬機,包括一套字節碼指令集、一組程序寄存器、一個虛擬機棧、一個虛擬機堆、一個方法區和一個垃圾回收器。JVM運行在操作系統之上,不與硬件設備直接交互。Java源文件在通過編譯器之后被編譯成相應的.Class文件(字節碼文件),.Class文件又被JVM中的解釋器編譯成機器碼在不同的操作系統(Windows、Linux、Mac)上運行。每種操作系統的解釋器都是不同的,但基于解釋器實現的虛擬機是相同的,這也是Java能夠跨平臺的原因。在一個Java進程開始運行后,虛擬機就開始實例化了,有多個進程啟動就會實例化多個虛擬機實例。進程退出或者關閉,則虛擬機實例消亡,在多個虛擬機實例之間不能共享數據。Java程序的具體運行過程如下。
(1)Java源文件被編譯器編譯成字節碼文件。
(2)JVM將字節碼文件編譯成相應操作系統的機器碼。
(3)機器碼調用相應操作系統的本地方法庫執行相應的方法
類加載器子系統用于將編譯好的.Class文件加載到JVM中;
◎ 運行時數據區用于存儲在JVM運行過程中產生的數據,包括程序計數器、方法區、本地方法區、虛擬機棧和虛擬機堆;
◎ 執行引擎包括即時編譯器和垃圾回收器,即時編譯器用于將Java字節碼編譯成具體的機器碼,垃圾回收器用于回收在運行過程中不再使用的對象;
◎ 本地接口庫用于調用操作系統的本地方法庫完成具體的指令操作。
直接內存(Direct Memory)
并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現,
。
在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,
因為避免了在Java堆和Native堆中來回復制數據。
顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但經常忽略直接內存,使得各個內存區域總和大于物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。
JVM后臺運行的線程
在JVM后臺會運行許多線程,其中一些是JVM自己創建和管理的線程,用于支持Java程序的執行和JVM的運行。以下是一些常見的JVM后臺運行的線程:
-
主線程(Main Thread): Java程序的入口點是主線程,程序從main方法開始執行,主線程負責啟動和執行Java程序的其他線程。
-
垃圾回收線程(Garbage Collection Threads): 垃圾回收線程是JVM中負責執行垃圾回收的線程。它們掃描堆內存,標記和清理不再使用的對象,從而釋放內存空間。
-
編譯線程(Compilation Threads): 編譯線程負責將Java源代碼編譯成可執行的字節碼。JVM中的即時編譯器將字節碼編譯為機器碼以提高運行效率。
-
信號分發線程(Signal Dispatcher Thread): 信號分發線程負責接收和分發操作系統發送的信號,如崩潰信號、用戶自定義信號等。
-
定時器線程(Timer Threads): 定時器線程負責調度和執行定時任務,它在后臺運行,監視計劃執行的任務,并在指定的時間觸發相應的操作。
除了上述線程之外,還有一些其他的線程用于執行特定的功能,例如監控線程、虛擬機內部線程等。這些線程都運行在JVM后臺,對于應用程序來說是透明的,由JVM負責創建、調度和管理。
JVM 常用參數
標準參數中比較有用的:
verbose -verbose:class 輸出jvm載入類的相關信息,當jvm報告說找不到類或者類沖突時可此進行診斷。 -verbose:gc 輸出每次GC的相關情況。 -verbose:jni 輸出native方法調用的相關情況,一般用于診斷jni調用錯誤信息。
非標準參數又稱為擴展參數,比較有用的是
-Xms512m 設置JVM促使內存為512m。此值可以設置與-Xmx相同,以避免每次垃圾回收完成后JVM重新分配內存。
-Xmx512m ,設置JVM最大可用內存為512M。
-Xmn200m:設置年輕代大小為200M。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,所以增大年輕代后,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。
-Xss128k:
設置每個線程的堆棧大小。JDK5.0以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。更具應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。
-Xloggc:file 與-verbose:gc功能類似,只是將每次GC事件的相關情況記錄到一個文件中,文件的位置最好在本地,以避免網絡的潛在問題。 若與verbose命令同時出現在命令行中,則以-Xloggc為準。 -Xprof
跟蹤正運行的程序,并將跟蹤數據在標準輸出輸出;適合于開發環境調試。
非Stable參數
用-XX作為前綴的參數列表在jvm中可能是不健壯的,SUN也不推薦使用,后續可能會在沒有通知的情況下就直接取消了;但是由于這些參數中的確有很多是對我們很有用的,比如我們經常會見到的-XX:PermSize、-XX:MaxPermSize等等;
首先來介紹行為參數:
參數及其默認值 | 描述 |
---|---|
-XX:-DisableExplicitGC | 禁止調用System.gc();但jvm的gc仍然有效 |
-XX:+MaxFDLimit | 最大化文件描述符的數量限制 |
-XX:+ScavengeBeforeFullGC | 新生代GC優先于Full GC執行 |
-XX:+UseGCOverheadLimit | 在拋出OOM之前限制jvm耗費在GC上的時間比例 |
-XX:-UseConcMarkSweepGC | 對老生代采用并發標記交換算法進行**GC** |
-XX:-UseParallelGC | 啟用并行**GC** |
-XX:-UseParallelOldGC | 對Full GC啟用并行,當-XX:-UseParallelGC啟用時該項自動啟用 |
-XX:-UseSerialGC | 啟用串行**GC** |
-XX:+UseThreadPriorities | 啟用本地線程優先級 |
上面表格中黑體的三個參數代表著jvm中GC執行的三種方式,即串行、并行、并發; 串行(**SerialGC)是jvm的默認GC方式,一般適用于小型應用和單處理器,算法比較簡單,GC效率也較高,但可能會給應用帶來停頓; 并行(ParallelGC)是指GC運行時,對應用程序運行沒有影響,GC和app兩者的線程在并發執行,這樣可以最大限度不影響app的運行; 并發(ConcMarkSweepGC)**是指多個線程并發執行GC,一般適用于多處理器系統中,可以提高GC的效率,但算法復雜,系統消耗較大;
性能調優參數列表:
參數及其默認值 | 描述 |
---|---|
-XX:LargePageSizeInBytes=4m | 設置用于Java堆的大頁面尺寸 |
-XX:MaxHeapFreeRatio=70 | GC后java堆中空閑量占的最大比例 |
-XX:MaxNewSize=size | 新生成對象能占用內存的最大值 |
-XX:MaxPermSize=64m | 老生代對象能占用內存的最大值 |
-XX:MinHeapFreeRatio=40 | GC后java堆中空閑量占的最小比例 |
-XX:NewRatio=2 | 新生代內存容量與老生代內存容量的比例 |
-XX:NewSize=2.125m | 新生代對象生成時占用內存的默認值 |
-XX:ReservedCodeCacheSize=32m | 保留代碼占用的內存容量 |
-XX:ThreadStackSize=512 | 設置線程棧大小,若為0則使用系統默認值 |
-XX:+UseLargePages | 使用大頁面內存 |
我們在日常性能調優中基本上都會用到以上黑體的這幾個屬性;
調試參數列表:
參數及其默認值 | 描述 |
---|---|
-XX:-CITime | 打印消耗在JIT編譯的時間 |
-XX:ErrorFile=./hs_err_pid<pid>.log | 保存錯誤日志或者數據到文件中 |
-XX:-ExtendedDTraceProbes | 開啟solaris特有的dtrace探針 |
-XX:HeapDumpPath=./java_pid<pid>.hprof | 指定導出堆信息時的路徑或文件名 |
-XX:-HeapDumpOnOutOfMemoryError | 當首次遭遇**OOM時導出此時堆中相關信息** |
-XX:OnError="<cmd args>;<cmd args>" | 出現致命ERROR之后運行自定義命令 |
-XX:OnOutOfMemoryError="<cmd args>;<cmd args>" | 當首次遭遇OOM時執行自定義命令 |
-XX:-PrintClassHistogram | 遇到Ctrl-Break后打印類實例的柱狀信息,與jmap -histo功能相同 |
-XX:-PrintConcurrentLocks | 遇到**Ctrl-Break后打印并發鎖的相關信息,與jstack -l功能相同** |
-XX:-PrintCommandLineFlags | 打印在命令行中出現過的標記 |
-XX:-PrintCompilation | 當一個方法被編譯時打印相關信息 |
-XX:-PrintGC | 每次GC時打印相關信息 |
-XX:-PrintGC Details | 每次GC時打印詳細信息 |
-XX:-PrintGCTimeStamps | 打印每次GC的時間戳 |
-XX:-TraceClassLoading | 跟蹤類的加載信息 |
-XX:-TraceClassLoadingPreorder | 跟蹤被引用到的所有類的加載信息 |
-XX:-TraceClassResolution | 跟蹤常量池 |
-XX:-TraceClassUnloading | 跟蹤類的卸載信息 |
-XX:-TraceLoaderConstraints | 跟蹤類加載器約束的相關信息 |
class初始化過程是什么?
首先類加載的機制過程分為5個部分:加載、驗證、準備、解析、初始化
類的初始化過程是指對類進行首次使用之前的準備和初始化操作。類的初始化包括以下幾個步驟:
-
加載(Loading):類的加載是指通過類加載器將類的字節碼文件加載到內存中,并創建一個對應的Class對象。加載過程包括查找類文件、將類文件字節碼讀取到內存,并創建對應的Class對象。
-
鏈接(Linking):
-
驗證(Verification):驗證過程是對加載的類進行驗證,確保類的正確性和安全性,包括驗證類的格式、語義等方面。
-
準備(Preparation):準備階段是為類的靜態變量(被
static
修飾的變量)分配內存,并進行默認初始化,即賦予默認值(比如0、null等)。 -
解析(Resolution):解析過程是將符號引用轉換為直接引用,使得執行字節碼時可以直接找到對應的目標。
-
-
初始化(Initialization):在初始化階段,執行類的初始化代碼,包括靜態變量的顯式賦值、靜態代碼塊中的代碼執行等。初始化階段在Java虛擬機中是被加鎖的,確保只有一個線程對類進行初始化。
類的初始化是按需進行的,只有在以下情況下才會觸發類的初始化:
-
創建類的實例對象
-
訪問類的靜態變量
-
調用類的靜態方法
-
使用反射操作該類時
需要注意的是,類的初始化是按照以上步驟依次進行的,一旦開始了類的初始化,就會按順序依次執行各個步驟,且僅進行一次。此外,子類的初始化會觸發父類的初始化過程。
值得一提的是,類的加載和初始化過程是由Java虛擬機控制的,開發人員在代碼中無法直接干預類的加載和初始化順序。
JVM內存模型如何分配的?
JVM的類加載階段 JVM結構
JVM性能調優的原則有哪些?
-
多數的Java應用不需要在服務器上進行GC優化,虛擬機內部已有很多優化來保證應用的穩定運行,所以不要為了調優而調優,不當的調優可能適得其反
-
在應用上線之前,先考慮將機器的JVM參數設置到最優(適合)
-
在進行GC優化之前,需要確認項目的架構和代碼等已經沒有優化空間。我們不能指望一個系統架構有缺陷或者代碼層次優化沒有窮盡的應用,通過GC優化令其性能達到一個質的飛躍
-
GC優化是一個系統而復雜的工作,沒有萬能的調優策略可以滿足所有的性能指標。GC優化必須建立在我們深入理解各種垃圾回收器的基礎上,才能有事半功倍的效果
-
處理吞吐量和延遲問題時,垃圾處理器能使用的內存越大,即java堆空間越大垃圾收集效果越好,應用運行也越流暢。這稱之為GC內存最大化原則
-
在這三個屬性(吞吐量、延遲、內存)中選擇其中兩個進行jvm調優,稱之為GC調優3選2
什么情況下需要JVM調優?
-
Heap內存(老年代)持續上漲達到設置的最大內存值
-
Full GC 次數頻繁
-
GC 停頓(Stop World)時間過長(超過1秒,具體值按應用場景而定)
-
應用出現OutOfMemory 等內存異常
-
應用出現OutOfDirectMemoryError等內存異常( failed to allocate 16777216 byte(s) of direct memory (used: 1056964615, max: 1073741824))
-
應用中有使用本地緩存且占用大量內存空間
-
系統吞吐量與響應性能不高或下降
-
應用的CPU占用過高不下或內存占用過高不下
在JVM調優時,你關注哪些指標?
-
吞吐量:用戶代碼時間 / (用戶代碼執行時間 + 垃圾回收時間)。是評價垃圾收集器能力的重要指標之一,是不考慮垃圾收集引起的停頓時間或內存消耗,垃圾收集器能支撐應用程序達到的最高性能指標。吞吐量越高算法越好。
-
低延遲:STW越短,響應時間越好。評價垃圾收集器能力的重要指標,度量標準是縮短由于垃圾收集引起的停頓時間或完全消除因垃圾收集所引起的停頓,避免應用程序運行時發生抖動。暫停時間越短算法越好
-
在設計(或使用)GC 算法時,我們必須確定我們的目標:一個 GC 算法只可能針對兩個目標之一(即只專注于最大吞吐量或最小暫停時間),或嘗試找到一個二者的折衷
-
MinorGC盡可能多的收集垃圾對象。我們把這個稱作MinorGC原則,遵守這一原則可以降低應用程序FullGC 的發生頻率。FullGC 較耗時,是應用程序無法達到延遲要求或吞吐量的罪魁禍首
-
堆大小調整的著手點、分析點:
-
統計Minor GC 持續時間
-
統計Minor GC 的次數
-
統計Full GC的最長持續時間
-
統計最差情況下Full GC頻率
-
統計GC持續時間和頻率對優化堆的大小是主要著手點
-
我們按照業務系統對延遲和吞吐量的需求,在按照這些分析我們可以進行各個區大小的調整
-
-
一般來說吞吐量優先的垃圾回收器:-XX:+UseParallelGC -XX:+UseParallelOldGC,即常規的(PS/PO)
-
響應時間優先的垃圾回收器:CMS、G1
JVM常用參數有哪些?
-
Xms 是指設定程序啟動時占用內存大小。一般來講,大點,程序會啟動的快一點,但是也可能會導致機器暫時間變慢
-
Xmx 是指設定程序運行期間最大可占用的內存大小。如果程序運行需要占用更多的內存,超出了這個設置值,就會拋出OutOfMemory異常
-
Xss 是指設定每個線程的堆棧大小。這個就要依據你的程序,看一個線程大約需要占用多少內存,可能會有多少線程同時運行等
-
-Xmn、-XX:NewSize/-XX:MaxNewSize、-XX:NewRatio
-
高優先級:-XX:NewSize/-XX:MaxNewSize
-
中優先級:-Xmn(默認等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?)
-
低優先級:-XX:NewRatio
-
-
如果想在日志中追蹤類加載與類卸載的情況,可以使用啟動參數 -XX:TraceClassLoading -XX:TraceClassUnloading
JVM常用性能調優工具有哪些?
-
MAT
-
提示可能的內存泄露的點
-
-
jvisualvm
-
jconsole
-
Arthas
-
show-busy-java-threads
-
https://github.com/oldratlee/useful-scripts/blob/master/docs/java.md#-show-busy-java-threads? ####
-
線上排查問題的一般流程是怎么樣的?
-
CPU占用過高排查流程
-
利用 top 命令可以查出占 CPU 最高的的進程pid ,如果pid為 9876
-
然后查看該進程下占用最高的線程id【top -Hp 9876】
-
假設占用率最高的線程 ID 為 6900,將其轉換為 16 進制形式 (因為 java native 線程以 16 進制形式輸出) 【printf '%x\n' 6900】
-
利用 jstack 打印出 java 線程調用棧信息【jstack 9876 | grep '0x1af4' -A 50 --color】,這樣就可以更好定位問題
-
-
內存占用過高排查流程
-
查找進程id: 【top -d 2 -c】
-
查看JVM堆內存分配情況:jmap -heap pid
-
查看占用內存比較多的對象 jmap -histo pid | head -n 100
-
查看占用內存比較多的存活對象 jmap -histo:live pid | head -n 100
-
什么情況下,會拋出OOM呢?
-
JVM98%的時間都花費在內存回收
-
每次回收的內存小于2%
滿足這兩個條件將觸發OutOfMemoryException,這將會留給系統一個微小的間隙以做一些Down之前的操作,比如手動打印Heap Dump。并不是內存被耗空的時候才拋出 ?
系統OOM之前都有哪些現象?
-
每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右,FullGC的時間也有之前的0.5s延長到4、5s
-
FullGC的次數越來越多,最頻繁時隔不到1分鐘就進行一次FullGC
-
老年代的內存越來越大并且每次FullGC后,老年代只有少量的內存被釋放掉
如何進行堆Dump文件分析?
可以通過指定啟動參數 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/app/data/dump/heapdump.hpro 在發生OOM的時候自動導出Dump文件 ?
如何進行GC日志分析?
為了方便分析GC日志信息,可以指定啟動參數 【-Xloggc: app-gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps】,方便詳細地查看GC日志信息
-
使用 【jinfo pid】查看當前JVM堆的相關參數
-
繼續使用 【jstat -gcutil 2315 1s 10】查看10s內當前堆的占用情況
-
也可以使用【jmap -heap pid】查看當前JVM堆的情況
-
我們可以繼續使用 【jmap -F -histo pid | head -n 20】,查看前20行打印,即查看當前top20的大對象,一般從這里可以發現一些異常的大對象,如果沒有,那么可以繼續排名前50的大對象,分析
-
最后使用【jmap -F -dump:file=a.bin pid】,如果dump文件很大,可以壓縮一下【tar -czvf a.tar.gz a.bin】
-
再之后,就是對dump文件進行分析了,使用MAT分析內存泄露
-
參考案例: https://www.lagou.com/lgeduarticle/142372.html
線上死鎖是如何排查的?
-
jps 查找一個可能有問題的進程id
-
然后執行 【jstack -F 進程id】
-
如果環境允許遠程連接JVM,可以使用jconsole或者jvisualvm,圖形化界面檢測是否存在死鎖
線上YGC耗時過長優化方案有哪些?
-
如果生命周期過長的對象越來越多(比如全局變量或者靜態變量等),會導致標注和復制過程的耗時增加
-
對存活對象標注時間過長:比如重載了Object類的Finalize方法,導致標注Final Reference耗時過長;或者String.intern方法使用不當,導致YGC掃描StringTable時間過長。可以通過以下參數顯示GC處理Reference的耗時-XX:+PrintReferenceGC
-
長周期對象積累過多:比如本地緩存使用不當,積累了太多存活對象;或者鎖競爭嚴重導致線程阻塞,局部變量的生命周期變長
-
案例參考: https://my.oschina.net/lishangzhi/blog/4703942
線上頻繁FullGC優化方案有哪些?
-
線上頻繁FullGC一般會有這么幾個特征:
-
線上多個線程的CPU都超過了100%,通過jstack命令可以看到這些線程主要是垃圾回收線程
-
通過jstat命令監控GC情況,可以看到Full GC次數非常多,并且次數在不斷增加
-
-
排查流程:
-
top找到cpu占用最高的一個 進程id
-
然后 【top -Hp 進程id】,找到cpu占用最高的 線程id
-
【printf "%x\n" 線程id 】,假設16進制結果為 a
-
jstack 線程id | grep '0xa' -A 50 --color
-
如果是正常的用戶線程, 則通過該線程的堆棧信息查看其具體是在哪處用戶代碼處運行比較消耗CPU
-
如果該線程是 VMThread,則通過 jstat-gcutil命令監控當前系統的GC狀況,然后通過 jmapdump:format=b,file=導出系統當前的內存數據。導出之后將內存情況放到eclipse的mat工具中進行分析即可得出內存中主要是什么對象比較消耗內存,進而可以處理相關代碼;正常情況下會發現VM Thread指的就是垃圾回收的線程
-
再執行【jstat -gcutil 進程id】, 看到結果,如果FGC的數量很高,且在不斷增長,那么可以定位是由于內存溢出導致FullGC頻繁,系統緩慢
-
然后就可以Dump出內存日志,然后使用MAT的工具分析哪些對象占用內存較大,然后找到對象的創建位置,處理即可
-
-
參考案例:面試官:如果你們的系統 CPU 突然飆升且 GC 頻繁,如何排查? ####
內存逃逸分析
1, 是JVM優化技術,它不是直接優化手段,而是為其它優化手段提供依據。
2,逃逸分析主要就是分析對象的動態作用域。
3,逃逸有兩種:方法逃逸和線程逃逸。
? 方法逃逸(對象逃出當前方法):當一個對象在方法里面被定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其它方法中。 ?線程逃逸((對象逃出當前線程):這個對象甚至可能被其它線程訪問到,例如賦值給類變量或可以在其它線程中訪問的實例變量 12345
4,如果不存在逃逸,則可以對這個變量進行優化
4.1. 棧上分配。 在一般應用中,不會逃逸的局部對象占比很大,如果使用棧上分配,那大量對象會隨著方法結束而自動銷毀,垃圾回收系統壓力就小很多。
4.2. 同步消除 線程同步本身比較耗時,如果確定一個變量不會逃逸出線程,無法被其它線程訪問到,那這個變量的讀寫就不會存在競爭,對這個變量的同步措施 可以清除。
4.3. 標量替換。
-
標量就是不可分割的量,java中基本數據類型,reference類型都是標量。相對的一個數據可以繼續分解,它就是聚合量(aggregate)。
-
如果把一個對象拆散,將其成員變量恢復到基本類型來訪問就叫做標量替換。
-
如果逃逸分析證明一個對象不會被外部訪問,并且這個對象可以被拆散的話,那么程序真正執行的時候將可能不創建這個對象,而改為直接在>棧上創建若干個成員變量。
5,逃逸分析還不成熟。 5.1,不能保證逃逸分析的性能收益必定高于它的消耗。 判斷一個對象是否逃逸耗時長,如果分析完發現沒有幾個不逃逸的對象,那時間就白白浪費了。 5.2,基于逃逸分析的優化手段不成熟,如上面提到的棧上分配,由于hotspot目前的實現方式導致棧上分配實現起來復雜。
6,相關JVM參數 -XX:+DoEscapeAnalysis 開啟逃逸分析 -XX:+PrintEscapeAnalysis 開啟逃逸分析后,可通過此參數查看分析結果。 -XX:+EliminateAllocations 開啟標量替換 -XX:+EliminateLocks 開啟同步消除 -XX:+PrintEliminateAllocations 開啟標量替換后,查看標量替換情況。
如何進行線上堆外內存泄漏的分析?(Netty尤其居多)
-
JVM的堆外內存泄露的定位一直是個比較棘手的問題
-
對外內存的泄漏分析一般都是先從堆內內存分析的過程中衍生出來的。有可能我們分析堆內內存泄露過程中發現,我們計算出來的JVM堆內存竟然大于了整個JVM的Xmx的大小,那說明多出來的是堆外內存
-
如果使用了 Netty 堆外內存,那么可以自行監控堆外內存的使用情況,不需要借助第三方工具,我們是使用的“反射”拿到的堆外內存的情況
-
逐漸縮小范圍,直到 Bug 被找到。當我們確認某個線程的執行帶來 Bug 時,可單步執行,可二分執行,定位到某行代碼之后,跟到這段代碼,然后繼續單步執行或者二分的方式來定位最終出 Bug 的代碼。這個方法屢試不爽,最后總能找到想要的 Bug
-
熟練掌握 idea 的調試,讓我們的“捉蟲”速度快如閃電(“閃電俠”就是這么來的)。這里,最常見的調試方式是預執行表達式,以及通過線程調用棧,死盯某個對象,就能夠掌握這個對象的定義、賦值之類
-
在使用直接內存的項目中,最好建議配置 -XX:MaxDirectMemorySize,設定一個系統實際可達的最大的直接內存的值,默認的最大直接內存大小等于 -Xmx的值
-
排查堆外泄露,建議指定啟動參數: -XX:NativeMemoryTracking=summary - Dio.netty.leakDetection.targetRecords=100-Dio.netty.leakDetection.level=PARANOID,后面兩個參數是Netty的相關內存泄露檢測的級別與采樣級別
-
參考案例: Netty堆外內存泄露排查盛宴 - 美團技術團隊
線上元空間內存泄露優化方案有哪些?
-
需要注意的一點是 Java8以及Java8+的JVM已經將永久代廢棄了,取而代之的是元空間,且元空間是不是在JVM堆中的,而屬于堆外內存,受最大物理內存限制。最佳實踐就是我們在啟動參數中最好設置上 -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m。具體的值根據情況設置。為避免動態申請,可以直接都設置為最大值
-
元空間主要存放的是類元數據,而且metaspace判斷類元數據是否可以回收,是根據加載這些類元數據的Classloader是否可以回收來判斷的,只要Classloader不能回收,通過其加載的類元數據就不會被回收。所以線上有時候會出現一種問題,由于框架中,往往大量采用類似ASM、javassist等工具進行字節碼增強,生成代理類。如果項目中由主線程頻繁生成動態代理類,那么就會導致元空間迅速占滿,無法回收
-
具體案例可以參見: 一次完整的JVM堆外內存泄漏故障排查記錄 - 知乎
java類加載器有哪些?
Bootstrap類加載器
啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實現的,沒有父類,是虛擬機自身的一部分,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數指定的路徑下的jar包加載到內存中,注意必由于虛擬機是按照文件名識別加載jar包的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭
Extention 類加載器
擴展類加載器是指Sun公司實現的sun.misc.Launcher$ExtClassLoader類,由Java語言實現的,父類加載器為null,是Launcher的靜態內部類,它負責加載<JAVA_HOME>/lib/ext目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者可以直接使用標準擴展類加載器 [
](深入理解Java類加載器(ClassLoader)_java classloader-CSDN博客)
Application類加載器
稱應用程序加載器是指 Sun公司實現的sun.misc.Launcher$AppClassLoader。父類加載器為ExtClassLoader,它負責加載系統類路徑java -classpath或-D java.class.path 指定路徑下的類庫,也就是我們經常用到的classpath路徑,開發者可以直接使用系統類加載器,一般情況下該類加載是程序中默認的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器 ?
Custom自定義類加載器
應用程序可以自定義類加載器,父類加載器為AppClassLoader
雙親委派機制是什么?
雙親委派機制
雙親委派機制(Parent Delegation Model)是Java類加載器的一種工作機制。它定義了類加載器的層次結構和類加載的行為,保證Java類的安全性和一致性。
在Java中,類加載器(ClassLoader)根據特定的規則來加載類的字節碼并將其轉換為可執行的Java類。雙親委派機制是指當類加載器在加載某個類時,首先將該請求委派給父類加載器來嘗試加載。只有當父類加載器無法加載時,才由當前類加載器來嘗試加載。這樣的委派關系一直往上追溯,直到達到最頂層的啟動類加載器。
雙親委派機制的優勢在于保證了類的統一性和安全性。當一個類加載器嘗試加載某個類時,它會先檢查是否被父類加載器加載過。如果父類加載器已經加載了該類,那么直接返回已加載的類,確保了類的唯一性。同時,通過這種機制可以防止惡意代碼通過自定義的類加載器來替換Java核心庫中的類,提高了系統的安全性。
總結來說,雙親委派機制采用了一種層次式的類加載器結構,它通過逐級委派的方式保證類加載的一致性和安全性。在使用Java類加載器時,了解雙親委派機制的工作原理對于理解類加載的過程和解決類加載問題非常有幫助。
如何破壞雙親委派模型
(一)雙親委派模型的第一次“被破壞”是重寫自定義加載器的loadClass(),jdk不推薦。一般都只是重寫findClass(),這樣可以保持雙親委派機制.而loadClass方法加載規則由自己定義,就可以隨心所欲的加載類
雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK 1.2發布之前。由于雙親委派模型在JDK 1.2之后才被引入,而類加載器和抽象類java.lang. ClassLoader則在JDK 1.0時代就已經存在,面對已經存在的用戶自定義類加載器的實現代碼,Java設計者引入雙親委派模型時不得不做出一些妥協。為了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一個新的protected方法findClass(),在此之前,用戶去繼承java. lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,因為虛擬機在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調用自己的loadClass()。
(二)雙親委派模型的第二次“被破壞”是ServiceLoader和Thread.setContextClassLoader()。即線程上下文類加載器(contextClassLoader)。雙親委派模型很好地解決了各個類加載器的基礎類統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以被稱為“基礎”,是因為它們總是作為被調用代碼調用的API。但是,
如果基礎類又要調用用戶的代碼,
那該怎么辦呢?線程上下文類加載器就出現了。
-
SPI。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個;如果在應用程序的全局范圍內都沒有設置過,那么這個類加載器默認就是應用程序類加載器。了有線程上下文類加載器,JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
-
線程上下文類加載器默認情況下就是AppClassLoader,那為什么不直接通過getSystemClassLoader()獲取類加載器來加載classpath路徑下的類的呢?其實是可行的,但這種直接使用getSystemClassLoader()方法獲取AppClassLoader加載類有一個缺點,那就是代碼部署到不同服務時會出現問題,如把代碼部署到Java Web應用服務或者EJB之類的服務將會出問題,因為這些服務使用的線程上下文類加載器并非AppClassLoader,而是Java Web應用服自家的類加載器,類加載器不同。,所以我們應用該少用getSystemClassLoader()。總之不同的服務使用的可能默認ClassLoader是不同的,但使用線程上下文類加載器總能獲取到與當前程序執行相同的ClassLoader,從而避免不必要的問題
(三)雙親委派模型的第三次“被破壞”是由于用戶對程序動態性的追求導致的,這里所說的“動態性”指的是當前一些非常“熱門”的名詞:代碼熱替換、模塊熱部署等,簡答的說就是機器不用重啟,只要部署上就能用。
OSGi實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每一個程序模塊(OSGi中稱為Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。
GC如何判斷對象可以被回收?
-
垃圾收集器(Garbage Collector)會在運行時自動判斷對象是否可以被回收。下面是一些常見的對象回收判斷策略:
-
引用計數法(Reference Counting):每個對象都有一個引用計數器,當有一個新的引用指向對象時,計數器加1;當引用失效時,計數器減1。當計數器為0時,表示對象不再被引用,可以被回收。但引用計數法無法解決循環引用的情況。
-
可達性分析算法(Reachability Analysis):Java虛擬機使用可達性分析算法來判斷對象是否可能被程序引用。從GC Roots(如被活動線程棧引用的對象、靜態變量引用的對象等)開始,通過對象之間的引用關系進行遍歷,無法到達的對象即為不可達對象,可以被回收。
-
那么GcRoot有哪些?
-
虛擬機棧中引用的對象
-
方法區中靜態變量引用的對象。
-
方法區中常量引用的對象
-
本地方法棧中(即一般說的native方法)引用的對象
回收機制是
-
強引用:通過關鍵字new的對象就是強引用對象,強引用指向的對象任何時候都不會被回收,寧愿OOM也不會回收。
-
軟引用:如果一個對象持有軟引用,那么當JVM堆空間不足時,會被回收。一個類的軟引用可以通過java.lang.ref.SoftReference持有。
-
弱引用:如果一個對象持有弱引用,那么在GC時,只要發現弱引用對象,就會被回收。一個類的弱引用可以通過java.lang.ref.WeakReference持有。
-
虛引用:幾乎和沒有一樣,隨時可以被回收。通過PhantomReference持有。
如何回收內存對象,有哪些回收算法?
1.標記-清除(Mark-Sweep)算法
分為標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
它的主要不足有兩個:
-
效率問題,標記和清除兩個過程的效率都不高。
-
空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
-
復制算法
為了解決效率問題,一種稱為復制(Copying)的收集算法出現了,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
復制算法的代價是將內存縮小為了原來的一半,減少了實際可用的內存。現在的商業虛擬機都采用這種收集算法來回收新生代,IBM公司的專門研究表明,新生代中的對象98%是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保(Handle Promotion)。
-
標記-整理算法
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。根據老年代的特點,有人提出了另外一種標記-整理(Mark-Compact)算法,標記過程仍然與標記-清除算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
-
分代收集算法
當前商業虛擬機的垃圾收集都采用分代收集(Generational Collection)算法,這種算法并沒有什么新的思想,只是根據對象存活周期的不同將內存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清理或者標記—整理算法來進行回收。
jvm有哪些垃圾回收器,實際中如何選擇?
圖中展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,則說明它們可以搭配使用。虛擬機所處的區域則表示它是屬于新生代還是老年代收集器。 新生代收集器(全部的都是復制算法):Serial、ParNew、Parallel Scavenge 老年代收集器:CMS(標記-清理)、Serial Old(標記-整理)、Parallel Old(標記整理) 整堆收集器: G1(一個Region中是標記-清除算法,2個Region之間是復制算法) 同時,先解釋幾個名詞: 1,并行(Parallel):多個垃圾收集線程并行工作,此時用戶線程處于等待狀態 2,并發(Concurrent):用戶線程和垃圾收集線程同時執行 3,吞吐量:運行用戶代碼時間/(運行用戶代碼時間+垃圾回收時間) 1.Serial收集器是最基本的、發展歷史最悠久的收集器。 特點:單線程、簡單高效(與其他收集器的單線程相比),對于限定單個CPU的環境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程手機效率。收集器進行垃圾回收時,必須暫停其他所有的工作線程,直到它結束(Stop The World)。 應用場景:適用于Client模式下的虛擬機。 Serial / Serial Old收集器運行示意圖
2.ParNew收集器其實就是Serial收集器的多線程版本。 除了使用多線程外其余行為均和Serial收集器一模一樣(參數控制、收集算法、Stop The World、對象分配規則、回收策略等)。 特點:多線程、ParNew收集器默認開啟的收集線程數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。 和Serial收集器一樣存在Stop The World問題 應用場景:ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為它是除了Serial收集器外,唯一一個能與CMS收集器配合工作的。 ParNew/Serial Old組合收集器運行示意圖如下:
3.Parallel Scavenge 收集器與吞吐量關系密切,故也稱為吞吐量優先收集器。 特點:屬于新生代收集器也是采用復制算法的收集器,又是并行的多線程收集器(與ParNew收集器類似)。 該收集器的目標是達到一個可控制的吞吐量。還有一個值得關注的點是:GC自適應調節策略(與ParNew收集器最重要的一個區別) GC自適應調節策略:Parallel Scavenge收集器可設置-XX:+UseAdptiveSizePolicy參數。當開關打開時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等,虛擬機會根據系統的運行狀況收集性能監控信息,動態設置這些參數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略。 Parallel Scavenge收集器使用兩個參數控制吞吐量:
-
XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間
-
XX:GCRatio 直接設置吞吐量的大小。
4.Serial Old是Serial收集器的老年代版本。 特點:同樣是單線程收集器,采用標記-整理算法。 應用場景:主要也是使用在Client模式下的虛擬機中。也可在Server模式下使用。 Server模式下主要的兩大用途(在后續中詳細講解···):
-
在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用。
-
作為CMS收集器的后備方案,在并發收集Concurent Mode Failure時使用。
Serial / Serial Old收集器工作過程圖(Serial收集器圖示相同):
5.Parallel Old是Parallel Scavenge收集器的老年代版本。 特點:多線程,采用標記-整理算法。 應用場景:注重高吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old 收集器。 Parallel Scavenge/Parallel Old收集器工作過程圖:
6.CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。
特點:基于標記-清除算法實現。并發收集、低停頓。
應用場景:適用于注重服務的響應速度,希望系統停頓時間最短,給用戶帶來更好的體驗等場景下。如web程序、b/s服務。
CMS收集器的運行過程分為下列4步:
初始標記:標記GC Roots能直接到的對象。速度很快但是仍存在Stop The World問題。
并發標記:進行GC Roots Tracing 的過程,找出存活對象且用戶線程可并發執行。
重新標記:為了修正并發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄。仍然存在Stop The World問題。
并發清除:對標記的對象進行清除回收。 CMS收集器的內存回收過程是與用戶線程一起并發執行的。 CMS收集器的工作過程圖:
CMS收集器的缺點:
-
對CPU資源非常敏感。
-
無法處理浮動垃圾,可能出現Concurrent Model Failure失敗而導致另一次Full GC的產生。
-
因為采用標記-清除算法所以會存在空間碎片的問題,導致大對象無法分配空間,不得不提前觸發一次Full GC。
?
7.G1收集器一款面向服務端應用的垃圾收集器。
特點如下:
并行與并發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World停頓時間。部分收集器原本需要停頓Java線程來執行GC動作,G1收集器仍然可以通過并發的方式讓Java程序繼續運行。
分代收集:G1能夠獨自管理整個Java堆,并且采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。
空間整合:G1運作期間不會產生空間碎片,收集后能提供規整的可用內存。
可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型。能讓使用者明確指定在一個長度為M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒。 G1收集器運行示意圖:
?
關于gc的選擇 除非應用程序有非常嚴格的暫停時間要求,否則請先運行應用程序并允許VM選擇收集器(如果沒有特別要求。使用VM提供給的默認GC就好)。 如有必要,調整堆大小以提高性能。 如果性能仍然不能滿足目標,請使用以下準則作為選擇收集器的起點:
-
如果應用程序的數據集較小(最大約100 MB),則選擇帶有選項-XX:+ UseSerialGC的串行收集器。
-
如果應用程序將在單個處理器上運行,并且沒有暫停時間要求,則選擇帶有選項-XX:+ UseSerialGC的串行收集器。
-
如果(a)峰值應用程序性能是第一要務,并且(b)沒有暫停時間要求或可接受一秒或更長時間的暫停,則讓VM選擇收集器或使用-XX:+ UseParallelGC選擇并行收集器 。
-
如果響應時間比整體吞吐量更重要,并且垃圾收集暫停時間必須保持在大約一秒鐘以內,則選擇具有-XX:+ UseG1GC。(值得注意的是JDK9中CMS已經被Deprecated,不可使用!移除該選項)
-
如果使用的是jdk8,并且堆內存達到了16G,那么推薦使用G1收集器,來控制每次垃圾收集的時間。
-
如果響應時間是高優先級,或使用的堆非常大,請使用-XX:UseZGC選擇完全并發的收集器。(值得注意的是JDK11開始可以啟動ZGC,但是此時ZGC具有實驗性質,在JDK15中[202009發布]才取消實驗性質的標簽,可以直接顯示啟用,但是JDK15默認GC仍然是G1)
這些準則僅提供選擇收集器的起點,因為性能取決于堆的大小,應用程序維護的實時數據量以及可用處理器的數量和速度。 如果推薦的收集器沒有達到所需的性能,則首先嘗試調整堆和新生代大小以達到所需的目標。 如果性能仍然不足,嘗試使用其他收集器 總體原則:減少STOP THE WORD時間,使用并發收集器(比如CMS+ParNew,G1)來減少暫停時間,加快響應時間,并使用并行收集器來增加多處理器硬件上的總體吞吐量。
JVM8為什么要增加元空間?
原因: 1、字符串存在永久代中,容易出現性能問題和內存溢出。 2、類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。 3、永久代會為 GC 帶來不必要的復雜度,并且回收效率偏低。
JVM8中元空間有哪些特點?
1,每個加載器有專門的存儲空間。 2,不會單獨回收某個類。 3,元空間里的對象的位置是固定的。 4,如果發現某個加載器不再存貨了,會把相關的空間整個回收
如何解決線上gc頻繁的問題?
-
查看監控,以了解出現問題的時間點以及當前FGC的頻率(可對比正常情況看頻率是否正常)
-
了解該時間點之前有沒有程序上線、基礎組件升級等情況。
-
了解JVM的參數設置,包括:堆空間各個區域的大小設置,新生代和老年代分別采用了哪些垃圾收集器,然后分析JVM參數設置是否合理。
-
再對步驟1中列出的可能原因做排除法,其中元空間被打滿、內存泄漏、代碼顯式調用gc方法比較容易排查。
-
針對大對象或者長生命周期對象導致的FGC,可通過 jmap -histo 命令并結合dump堆內存文件作進一步分析,需要先定位到可疑對象。
-
通過可疑對象定位到具體代碼再次分析,這時候要結合GC原理和JVM參數設置,弄清楚可疑對象是否滿足了進入到老年代的條件才能下結論。
內存溢出的原因有哪些,如何排查線上問題?
-
java.lang.OutOfMemoryError: ......java heap space..... ? 堆棧溢出,代碼問題的可能性極大
-
java.lang.OutOfMemoryError: GC over head limit exceeded 系統處于高頻的GC狀態,而且回收的效果依然不佳的情況,就會開始報這個錯誤,這種情況一般是產生了很多不可以被釋放的對象,有可能是引用使用不當導致,或申請大對象導致,但是java heap space的內存溢出有可能提前不會報這個錯誤,也就是可能內存就直接不夠導致,而不是高頻GC.
-
java.lang.OutOfMemoryError: PermGen space jdk1.7之前才會出現的問題 ,原因是系統的代碼非常多或引用的第三方包非常多、或代碼中使用了大量的常量、或通過intern注入常量、或者通過動態代碼加載等方法,導致常量池的膨脹
-
java.lang.OutOfMemoryError: Direct buffer memory ? ?直接內存不足,因為jvm垃圾回收不會回收掉直接內存這部分的內存,所以可能原因是直接或間接使用了ByteBuffer中的allocateDirect方法的時候,而沒有做clear
-
java.lang.StackOverflowError - ? ? Xss設置的太小了
-
java.lang.OutOfMemoryError: unable to create new native thread 堆外內存不足,無法為線程分配內存區域
-
java.lang.OutOfMemoryError: request {} byte for {}out of swap 地址空間不夠