天天看點

JVM記憶體管理機制&線上問題排查

本文主要基于“深入java虛拟機”這本書總結JVM的記憶體管理機制,并總結了常見的線上問題分析思路。文章最後面是我對線上故障思考的ppt總結。

Java記憶體區域

虛拟機運作時資料區如下圖所示:

JVM記憶體管理機制&線上問題排查

15291199000153.jpg

方法區:方法區又稱為永生代(Permanent Generation)是線程共享的記憶體區域。它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。當方法區記憶體溢出時報OOM:PermGen space。編譯器生成的各種位元組碼和符号引用存放在運作時常量池中。

堆:Java堆是Java虛拟機所管理的記憶體中最大的一塊,所有線程共享。此記憶體區域唯一的目的是存放對象執行個體。幾乎所有的對象執行個體(非基礎類型)都在這裡配置設定記憶體。Java堆還可以細分為新生代和老年代,其中新生代又可以分為Eden空間、From Survior空間、To Survior空間,對應的預設比例是8:1:1。在GC開始的時候,對象隻會存在于Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡門檻值,可以通過-XX:MaxTenuringThreshold來設定)的對象會被移動到年老代中,沒有達到門檻值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。

虛拟機棧:虛拟機棧是線程私有的,虛拟機棧描述的是java執行的記憶體模型,每個方法在執行的時候都會建立一個棧幀用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊,每個方法調用到執行的過程對應一個棧幀入棧到出棧的過程。

程式計數區:虛拟機處理多線程時,是通過輪流的切換線程,來擷取cpu的執行機會的。在虛拟機執行程式的過程中,當線程執行到某一位置時,虛拟機将cpu的執行機會出讓給了其他線程,此時原有線程的執行位置需要被記錄下來,而新得到執行機會的線程,又需要提供上次執行的位置,以此來保證程式中的多個線程可以持續的并行的執行下去。程式計數器的作用就是将各個線程下次所執行的(位元組碼)行号(準确來說是指令的位址)記錄下來,以保證其下次執行時可以正确的執行。程式計數器隻記錄位元組碼的行号,是以當線程執行本地方法(Native method)時,計數器的值是空。程式計數器所耗費的記憶體空間非常小,是以這個區域是不會抛出OutOfMemoryError錯誤的。

本地方法棧:與虛拟機棧的作用非常相似,隻是虛拟機棧為虛拟機執行Java方法服務,本地方法棧則為虛拟機使用的Native方法服務。

虛拟機運作時資料區之外的記憶體叫直接記憶體(Direct Memory),當我們使用NIO來,會調用Native方法直接配置設定堆外記憶體,通過一個存儲在java堆中的DirectByteBuffer對象被java程式使用。

垃圾收集器

确定對象存活算法

引用計數算法:當對象被引用,該對象的引用計數器+1,引用失效-1。目前主流的java虛拟機裡面都沒有選用引用計數算法來管理記憶體,最主要原因是它很難解決對象之間的循環引用問題。

可達性分析算法:當一個對象到GC Roots沒有任何引用鍊相連時,證明此對象可以回收。第一次GC時不可達對象可以通過finalize方法将自己變成可達進而避免被回收,第一次之後。GC Roots包括:1)虛拟機棧(棧幀中的本地變量表)中的引用對象;2)方法區中類靜态屬性引用的對象;3)方法區中常量引用的對象;4)本地方法棧中native方法引用的對象。

類回收條件:

  • 該類所有的執行個體都已經被回收
  • 加載該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class對象沒有任何地方被引用

垃圾收集算法

标記-清除算法:首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。該算法效率不高,而且産生大量不連續的記憶體碎片。

複制算法:将可用記憶體按容量分成大小相等的兩塊,每次隻使用一塊,當一塊用完了就将還存活的對象複制到另一塊上面,然後把使用完哪塊一次清理掉。效率高但可用記憶體為原來一半。适用于年輕代記憶體配置設定回收。

标記-整理算法:複制算法在存活率較高時需要進行較多的複制操作,效率變低。根據老年代的特定,提出标記-整理算法,标記出所有需要回收的對象,然後将所有存活對象移動到一端。

安全點

為了保證GC回收時GC ROOT到堆對象的引用關系圖的一緻性,采用“串行”執行來保證“原子性”(也就是停止所有線程 STOP THE WORLD)。由于全掃描所有對象的時間成本非常大,HotSpot虛拟機實作采用了一個稱為OopMap的資料結構來記錄哪些記憶體位址存放了對象引用,通過生成的彙編代碼可以看到OopMap存在編譯後的指令中。在OopMap的協助下,HotSpot可以快速且準确完成GC Roots枚舉,但一個很現實的問題随之而來:可能引起OopMap内容變化的指令非常多,如果為每一個指令都生成對應的OopMap,那将會需要大量的額外空間,這樣GC的空間成本也會很高。HotSpot隻是在“特定的位置”記錄了OopMap資訊。這些位置稱為“安全點”。安全點一般選在長時間執行的指令前,如方法調用、循環跳轉、異常跳轉等。在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不是安全點,就恢複線程,讓它“跑”到安全點上。有些線程處于“sleep狀态”或者“blocked狀态”,GC不可能等這些線程蘇醒,這時就引出“安全區”概念,在安全區的任意位置開始GC都是安全的。類似sleep等指令對應的就是安全區。

收集器名稱 适用堆記憶體區域 描述
Serial 新生代 使用複制算法,使用單線程去完成垃圾回收
ParNew 是Serial的多線程版本,在多核機器下充分利用了CPU
Parallel Scavenge 使用複制算法的收集器,是多線程的,Parallel Scavenge收集器的目的是為了更充分的利用CPU,保障使用者線程使用CPU的時間是一個固定比例。适用于背景任務系統
Serial Old 老年代 Serial Old是Serial收集器的老年代版本
Parallel Old Parallel Scavenge的老年代垃圾收集器。但使用多線程和“标記-整理”算法
CMS(Concurrent Mark Sweep) 基于“标記-清除”算法實作,以擷取最短回收停頓時間為目标的收集器。CMS垃圾收集過程分為:初始标記、并發标記、重新标記、并發清除。初始标記僅僅标記GC Roots能直接關聯對象,并發标記和使用者線程同時進行,重新标記則是為了修正并發标記期間使用者程式導緻産生變化的标記記錄。CMS隻需要在初始标記和重新标記STOP THE WORLD,是以停頓時間短。
G1 新生代&老年代 使用G1收集器時,Java堆記憶體劃分成多個大小相等的獨立區域(Region),新生代和老年代不再是實體隔離了,都是Region的一部分,整個運作過程和CMS很像,分初始标記、并發标記、最終标記、篩選回收。

HotSpot垃圾收集器組合方式

JVM記憶體管理機制&線上問題排查

15292053777241.jpg

記憶體配置設定與回收政策

新生代Eden:fromSurvivor:toSurvivor預設比例大小為8:1:1。對象優先配置設定在新生代的Eden區,每一次新生代GC(Minor GC)對象都是從Eden和from Survivor轉到to Survivor區,這時對象年齡+1,當對象年齡增加到一定程度(預設15),對象就被晉升到老年代中。大對象在新生代沒有空間時會直接建立到老年代區。

虛拟機監控工具簡介

名稱 主要作用
jps(JVN Process Status Tool) 顯示制定系統所有的HotSpot虛拟機程序,類似linux的ps指令
jstat(JVM Statistics Monitoring Tool) 用于收集HotSpot虛拟機各方面運作資料,可以顯示本地或遠端虛拟機程序中的類狀态、記憶體、垃圾收集、JIT編譯等運作資料
jinfo(Configuration Info for java) 顯示虛拟機配置資訊,主要用于查詢虛拟機啟動參數
jmap(Memory Map for java) 生成虛拟機的記憶體轉儲快照,在啟動參數重加-XX:+HeapDumpOnOutOfMemoryError參數,可以讓虛拟機在OOM異常之後自動生成dump檔案,dump檔案可以使用MAT工具進行分析
jhat(JVM Heap Dump Browser) 用于分析heapdump檔案,它會建立一個Http/Html伺服器,讓使用者可以在浏覽器上檢視分析結果。分析結果以包進行分組顯示,可以用于分析一些簡單的記憶體問題,更專業的還是推薦MAT
jstack(Stack Trace for Java) 即時顯示虛拟機的線程快照,可以用于定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導緻長時間等待等問題。
HsDis(HotSpot disassembler) JIT生成代碼反生成彙編語句,可以用于分析機器底層時怎麼了解執行我們的java語句。[ HSDIS安裝執行參考 ]

其實jdk提供了很多監控JVM運作狀态的接口,市場上大部分線上排除工具、分析工具都是基于Instrumentation和Attach相關接口實作的。

基于Instrumentation可以用獨立于應用程式之外的代理(agent)程式來監測和協助運作在JVM上的應用程式。這種監測和協助包括但不限于擷取JVM運作時狀态,替換和修改類定義等。 Instrumentation 的最大作用就是類定義的動态改變和操作。Instrumentation結合位元組碼程式設計可以無侵入的實作線上java伺服器的監控。

Attach Api家族的成員非常的少。這裡我們隻關注2個類,”VirtualMachine” and “AttachProvider”,AttachProvider 的實作是針對不同的操作來使用的。正如他的名字提到的, AttachProvider針對每種不同的作業系統提供(provide)一個可以通路的 VirtualMachine的入口。

關于如何利用Instrumentation和Attach接口實作JVM虛拟機監控以及線上排查工具的實作,我後面會有單獨的文章剖析。

Java線上問題分析

線上問題是每個程式員在開發過程中不可避免的,線上問題在任何公司都存在,我們能做的隻是降低出現的機率和快速定位解決問題。開發者對線上釋出必須要有敬畏心,同時也不要怕遇到線上問題。我們總是在發現bug,解決bug中成長的。

我個人将線上問題可以分為以下四類:

  • 網絡相關類
  • 應用性能類
  • 機器性能類
  • 應用邏輯類

網絡相關異常

當我們從系統日志中發現SocketException、ConnectException、SoketTimeoutException、UnknownHostException、BindException等與網絡相關異常時,先通過ping或者telnet(或者通過nc –v {ip} {port})等工具檢測以下相應的ip端口是否通。這類問題我們一般找運維配置相關環境。網絡相關異常一般跟Java虛拟機無關,這裡我不再深入分析。

應用性能相關的異常又可以分為以下四類,我們逐一分析:

  • 運作類異常
  • 應用沒響應
  • 調用逾時
  • 記憶體溢出

現象:當應用日志中出現NoSuchMethodException、ClassNotFoundException、NoClassDefFoundError、ClassCastException等相關異常時。

常見原因:

1)經常遇到的包沖突

2)Java ClassLoader機制引起的加載順序問題

排查方法:

1)加載順序:在應用啟動的Vm參數中添加-XX:+TraceClassLoading 檢視應用啟動加載的jar包資訊

2)包沖突:通過mvn dependency:tree 列印依賴樹

現象:http傳回499、502、504等異常碼

1)java程序退出

2)資源被耗光(CPU、記憶體,這種後面單獨說)

3)死鎖

4)處理線程池耗光

1)死鎖:通過jstack –l 列印目前jvm中的所有堆棧資訊,檢視”wating”狀态的線程是否存在“目前線程locking的資源正式另一個線程wating的資源”的環形等待

2)處理線程池耗光:通過jstack –l檢視相關線程數

3)java程序退出:jps或者ps aux|grep “java”檢視有沒有相關程序

現象:業務日志各種TimeoutException異常

1)服務端響應慢

2)調用端或者服務端存在FullGC

3)調用端或者服務端load比較高(後面單獨說)

4)網絡問題(參照之前的方案)

先通過公司的服務鍊路監控檢視相應調用的調用鍊路耗時,找到異常的服務。再登上對應應用的伺服器檢視機器的負載資訊和服務相應的GC日志。如果伺服器load比較高,需要檢視伺服器IO、CPU、丢包率等更細的名額定為出是哪項資源存在瓶頸,結合伺服器流量、操作行為(通路磁盤頻率、通路檔案大小)定為出具體問題。如果GC比較頻繁,那就dump一份記憶體,分析一下是不是存在記憶體洩漏或者大量複雜對象等原因。

現象:業務日志出現java.lang.OutOfMemoryError異常,OOM後面可能跟着

1)GC overhead limit exceeded java heap space(堆溢出)

2)Unable to create new native thread(無法建立線程)

3)PermGen Space(永生代異常)

4)Direct buffer memory(直接記憶體溢出)

1)Java Heap配置設定不出需要的記憶體,存在記憶體洩漏

2)線程數超過了ulimit限制或者線程數超過了kernel.pid_max

3)加載的類、常量等資訊超過JVM中永生代的記憶體限制

4)ByteBuffer.allocateDirect申請的記憶體塊超過 –Xmx的大小

1)堆溢出:通過-XX:+HeapDumpOnOutOfMemeryError拿到記憶體dump檔案或者jmap –dump:file=<檔案名>,format=b pid 拿到HeapDump檔案,然後通過MAT 相關工具分析上面得到的HeapDump檔案

2)無法建立線程:ps -eLf|grep java –c 檢視目前所有的線程數 和 cat /proc/[pid]/limits 檢視某個程序的資源限制

3)永生代異常:調大PermSize

4)直接記憶體:通過-XX:MaxDirectMemorySize 調節大小

機器性能類異常

伺服器性能又展現在CPU、記憶體、磁盤IO三塊。下面逐個分析

CPU核心名額

us :使用者空間占用CPU百分比</br>

sy : 核心空間占用CPU百分比

wa :等待輸入輸出的CPU時間百分比

load: 綜合名額,指的是運作隊列(run-queue)的長度(等待程序的數目 + 運作程序的數目)

應用記憶體核心名額

VIRT: 目前程序對虛拟記憶體使用量。

RES:目前程序的實體記憶體使用量。

SHR:目前程序的共享記憶體使用量。

磁盤IO

r/s:每秒發送到裝置的讀入請求數.</br>

w/s:每秒發送到裝置的寫入請求數.</br>

rsec/s:每秒從裝置讀入的扇區數.</br>

wsec/s:每秒向裝置寫入的扇區數.

await:I/O請求平均執行時間,包括發送請求和執行的時間,機關是毫秒.

%util:在I/O請求發送到裝置期間,占用CPU時間的百分比,用于顯示裝置的帶寬使用率。當這個值接近100%時,表示裝置帶寬已經占滿.

常見問題

us高:代碼中出現非常耗CPU的操作或者出現頻繁的FullGC

sy高:鎖競争激烈,線程切換頻繁

iowait高:io讀寫操作頻繁

load高:一般根據cpu數量去判斷,Load值大于CPU的數量才算高。load是可以了解為一個綜合名額,一般伴随着CPU、IO異常一起出現。滿足以下條件就會進入CPU執行等待隊列,就會被load值統計進去:1)它沒有在等待I/O操作的結果;2)它沒有主動進入等待狀态(也就是沒有調用’wait’);3)沒有被停止(例如:等待終止)

檢視這些參數的指令

top (-H):top可以實時的觀察cpu的名額狀況,尤其是每個core的名額狀況,可以更有效的來幫助解決問題,-H則有助于看是什麼線程造成的CPU消耗,這對解決一些簡單的耗CPU的問題會有很大幫助。

Sar:sar有助于檢視曆史名額資料,除了CPU外,其他記憶體,磁盤,網絡等等各種名額都可以檢視,畢竟大部分時候問題都發生在過去,是以翻曆史記錄非常重要。

PS:所有的問題都需要具體分析,但是問題分析的前提是我們要知道各個名額的确切定義,不然容易丢失關鍵資訊而一直無法發現真正原因。

業務邏輯異常

其實我們遇到90%以上的線上問題都是邏輯問題,邏輯問題在本地我們可以通過工具一行一行debug确定問題。本地環境和線上環境一般情況下不互通,需要跳闆機中轉,同時遠端DEBUG很有可能将其他正常的業務請求攔下,影響其他使用者的使用。推薦一款很好用的線上排查工具

https://yq.aliyun.com/articles/2390 grace

,grace文檔的使用說明已經很詳細,我不再累述,線上排查的原理我後面會有單獨的文章分析。

線上故障思考PPT

JVM記憶體管理機制&amp;線上問題排查

image.png

JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
JVM記憶體管理機制&amp;線上問題排查
上一篇: SSLClient 工具