JVM内存管理机制线上问题排查
本文主要基于“深入java虛擬機”這本書總結JVM的內存管理機制,并總結了常見的線上問題分析思路。文章最后面是我對線上故障思考的ppt總結。
Java內存區域
虛擬機運行時數據區如下圖所示:
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垃圾收集器組合方式
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很有可能將其他正常的業務請求攔下,影響其他用戶的使用。推薦一款很好用的在線排查工具grace,grace文檔的使用說明已經很詳細,我不再累述,在線排查的原理我后面會有單獨的文章分析。
線上故障思考PPT
image.pngimage.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
總結
以上是生活随笔為你收集整理的JVM内存管理机制线上问题排查的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【跃迁之路】【497天】程序员高效学习方
- 下一篇: 几个基于jvm 的微服务框架