JVM内存管理机制线上问题排查
本文主要基于“深入java虛擬機(jī)”這本書總結(jié)JVM的內(nèi)存管理機(jī)制,并總結(jié)了常見的線上問題分析思路。文章最后面是我對線上故障思考的ppt總結(jié)。
Java內(nèi)存區(qū)域
虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)如下圖所示:
15291199000153.jpg方法區(qū):方法區(qū)又稱為永生代(Permanent Generation)是線程共享的內(nèi)存區(qū)域。它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。當(dāng)方法區(qū)內(nèi)存溢出時(shí)報(bào)OOM:PermGen space。編譯器生成的各種字節(jié)碼和符號引用存放在運(yùn)行時(shí)常量池中。
堆:Java堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊,所有線程共享。此內(nèi)存區(qū)域唯一的目的是存放對象實(shí)例。幾乎所有的對象實(shí)例(非基礎(chǔ)類型)都在這里分配內(nèi)存。Java堆還可以細(xì)分為新生代和老年代,其中新生代又可以分為Eden空間、From Survior空間、To Survior空間,對應(yīng)的默認(rèn)比例是8:1:1。在GC開始的時(shí)候,對象只會(huì)存在于Eden區(qū)和名為“From”的Survivor區(qū),Survivor區(qū)“To”是空的。緊接著進(jìn)行GC,Eden區(qū)中所有存活的對象都會(huì)被復(fù)制到“To”,而在“From”區(qū)中,仍存活的對象會(huì)根據(jù)他們的年齡值來決定去向。年齡達(dá)到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設(shè)置)的對象會(huì)被移動(dòng)到年老代中,沒有達(dá)到閾值的對象會(huì)被復(fù)制到“To”區(qū)域。經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個(gè)時(shí)候,“From”和“To”會(huì)交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。
虛擬機(jī)棧:虛擬機(jī)棧是線程私有的,虛擬機(jī)棧描述的是java執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息,每個(gè)方法調(diào)用到執(zhí)行的過程對應(yīng)一個(gè)棧幀入棧到出棧的過程。
程序計(jì)數(shù)區(qū):虛擬機(jī)處理多線程時(shí),是通過輪流的切換線程,來獲取cpu的執(zhí)行機(jī)會(huì)的。在虛擬機(jī)執(zhí)行程序的過程中,當(dāng)線程執(zhí)行到某一位置時(shí),虛擬機(jī)將cpu的執(zhí)行機(jī)會(huì)出讓給了其他線程,此時(shí)原有線程的執(zhí)行位置需要被記錄下來,而新得到執(zhí)行機(jī)會(huì)的線程,又需要提供上次執(zhí)行的位置,以此來保證程序中的多個(gè)線程可以持續(xù)的并行的執(zhí)行下去。程序計(jì)數(shù)器的作用就是將各個(gè)線程下次所執(zhí)行的(字節(jié)碼)行號(準(zhǔn)確來說是指令的地址)記錄下來,以保證其下次執(zhí)行時(shí)可以正確的執(zhí)行。程序計(jì)數(shù)器只記錄字節(jié)碼的行號,因此當(dāng)線程執(zhí)行本地方法(Native method)時(shí),計(jì)數(shù)器的值是空。程序計(jì)數(shù)器所耗費(fèi)的內(nèi)存空間非常小,因此這個(gè)區(qū)域是不會(huì)拋出OutOfMemoryError錯(cuò)誤的。
本地方法棧:與虛擬機(jī)棧的作用非常相似,只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù),本地方法棧則為虛擬機(jī)使用的Native方法服務(wù)。
虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)之外的內(nèi)存叫直接內(nèi)存(Direct Memory),當(dāng)我們使用NIO來,會(huì)調(diào)用Native方法直接分配堆外內(nèi)存,通過一個(gè)存儲(chǔ)在java堆中的DirectByteBuffer對象被java程序使用。
垃圾收集器
確定對象存活算法
引用計(jì)數(shù)算法:當(dāng)對象被引用,該對象的引用計(jì)數(shù)器+1,引用失效-1。目前主流的java虛擬機(jī)里面都沒有選用引用計(jì)數(shù)算法來管理內(nèi)存,最主要原因是它很難解決對象之間的循環(huán)引用問題。
可達(dá)性分析算法:當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈相連時(shí),證明此對象可以回收。第一次GC時(shí)不可達(dá)對象可以通過finalize方法將自己變成可達(dá)從而避免被回收,第一次之后。GC Roots包括:1)虛擬機(jī)棧(棧幀中的本地變量表)中的引用對象;2)方法區(qū)中類靜態(tài)屬性引用的對象;3)方法區(qū)中常量引用的對象;4)本地方法棧中native方法引用的對象。
類回收條件:
- 該類所有的實(shí)例都已經(jīng)被回收
- 加載該類的ClassLoader已經(jīng)被回收
- 該類對應(yīng)的java.lang.Class對象沒有任何地方被引用
垃圾收集算法
標(biāo)記-清除算法:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。該算法效率不高,而且產(chǎn)生大量不連續(xù)的內(nèi)存碎片。
復(fù)制算法:將可用內(nèi)存按容量分成大小相等的兩塊,每次只使用一塊,當(dāng)一塊用完了就將還存活的對象復(fù)制到另一塊上面,然后把使用完哪塊一次清理掉。效率高但可用內(nèi)存為原來一半。適用于年輕代內(nèi)存分配回收。
標(biāo)記-整理算法:復(fù)制算法在存活率較高時(shí)需要進(jìn)行較多的復(fù)制操作,效率變低。根據(jù)老年代的特定,提出標(biāo)記-整理算法,標(biāo)記出所有需要回收的對象,然后將所有存活對象移動(dòng)到一端。
安全點(diǎn)
為了保證GC回收時(shí)GC ROOT到堆對象的引用關(guān)系圖的一致性,采用“串行”執(zhí)行來保證“原子性”(也就是停止所有線程 STOP THE WORLD)。由于全掃描所有對象的時(shí)間成本非常大,HotSpot虛擬機(jī)實(shí)現(xiàn)采用了一個(gè)稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來記錄哪些內(nèi)存地址存放了對象引用,通過生成的匯編代碼可以看到OopMap存在編譯后的指令中。在OopMap的協(xié)助下,HotSpot可以快速且準(zhǔn)確完成GC Roots枚舉,但一個(gè)很現(xiàn)實(shí)的問題隨之而來:可能引起OopMap內(nèi)容變化的指令非常多,如果為每一個(gè)指令都生成對應(yīng)的OopMap,那將會(huì)需要大量的額外空間,這樣GC的空間成本也會(huì)很高。HotSpot只是在“特定的位置”記錄了OopMap信息。這些位置稱為“安全點(diǎn)”。安全點(diǎn)一般選在長時(shí)間執(zhí)行的指令前,如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等。在GC發(fā)生時(shí),首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不是安全點(diǎn),就恢復(fù)線程,讓它“跑”到安全點(diǎn)上。有些線程處于“sleep狀態(tài)”或者“blocked狀態(tài)”,GC不可能等這些線程蘇醒,這時(shí)就引出“安全區(qū)”概念,在安全區(qū)的任意位置開始GC都是安全的。類似sleep等指令對應(yīng)的就是安全區(qū)。
垃圾收集器
| Serial | 新生代 | 使用復(fù)制算法,使用單線程去完成垃圾回收 |
| ParNew | 新生代 | 是Serial的多線程版本,在多核機(jī)器下充分利用了CPU |
| Parallel Scavenge | 新生代 | 使用復(fù)制算法的收集器,是多線程的,Parallel Scavenge收集器的目的是為了更充分的利用CPU,保障用戶線程使用CPU的時(shí)間是一個(gè)固定比例。適用于后臺(tái)任務(wù)系統(tǒng) |
| Serial Old | 老年代 | Serial Old是Serial收集器的老年代版本 |
| Parallel Old | 老年代 | Parallel Scavenge的老年代垃圾收集器。但使用多線程和“標(biāo)記-整理”算法 |
| CMS(Concurrent Mark Sweep) | 老年代 | 基于“標(biāo)記-清除”算法實(shí)現(xiàn),以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。CMS垃圾收集過程分為:初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清除。初始標(biāo)記僅僅標(biāo)記GC Roots能直接關(guān)聯(lián)對象,并發(fā)標(biāo)記和用戶線程同時(shí)進(jìn)行,重新標(biāo)記則是為了修正并發(fā)標(biāo)記期間用戶程序?qū)е庐a(chǎn)生變化的標(biāo)記記錄。CMS只需要在初始標(biāo)記和重新標(biāo)記STOP THE WORLD,所以停頓時(shí)間短。 |
| G1 | 新生代&老年代 | 使用G1收集器時(shí),Java堆內(nèi)存劃分成多個(gè)大小相等的獨(dú)立區(qū)域(Region),新生代和老年代不再是物理隔離了,都是Region的一部分,整個(gè)運(yùn)作過程和CMS很像,分初始標(biāo)記、并發(fā)標(biāo)記、最終標(biāo)記、篩選回收。 |
HotSpot垃圾收集器組合方式
15292053777241.jpg內(nèi)存分配與回收策略
新生代Eden:fromSurvivor:toSurvivor默認(rèn)比例大小為8:1:1。對象優(yōu)先分配在新生代的Eden區(qū),每一次新生代GC(Minor GC)對象都是從Eden和from Survivor轉(zhuǎn)到to Survivor區(qū),這時(shí)對象年齡+1,當(dāng)對象年齡增加到一定程度(默認(rèn)15),對象就被晉升到老年代中。大對象在新生代沒有空間時(shí)會(huì)直接創(chuàng)建到老年代區(qū)。
虛擬機(jī)監(jiān)控工具簡介
| jps(JVN Process Status Tool) | 顯示制定系統(tǒng)所有的HotSpot虛擬機(jī)進(jìn)程,類似linux的ps命令 |
| jstat(JVM Statistics Monitoring Tool) | 用于收集HotSpot虛擬機(jī)各方面運(yùn)行數(shù)據(jù),可以顯示本地或遠(yuǎn)程虛擬機(jī)進(jìn)程中的類狀態(tài)、內(nèi)存、垃圾收集、JIT編譯等運(yùn)行數(shù)據(jù) |
| jinfo(Configuration Info for java) | 顯示虛擬機(jī)配置信息,主要用于查詢虛擬機(jī)啟動(dòng)參數(shù) |
| jmap(Memory Map for java) | 生成虛擬機(jī)的內(nèi)存轉(zhuǎn)儲(chǔ)快照,在啟動(dòng)參數(shù)重加-XX:+HeapDumpOnOutOfMemoryError參數(shù),可以讓虛擬機(jī)在OOM異常之后自動(dòng)生成dump文件,dump文件可以使用MAT工具進(jìn)行分析 |
| jhat(JVM Heap Dump Browser) | 用于分析heapdump文件,它會(huì)建立一個(gè)Http/Html服務(wù)器,讓用戶可以在瀏覽器上查看分析結(jié)果。分析結(jié)果以包進(jìn)行分組顯示,可以用于分析一些簡單的內(nèi)存問題,更專業(yè)的還是推薦MAT |
| jstack(Stack Trace for Java) | 即時(shí)顯示虛擬機(jī)的線程快照,可以用于定位線程出現(xiàn)長時(shí)間停頓的原因,如線程間死鎖、死循環(huán)、請求外部資源導(dǎo)致長時(shí)間等待等問題。 |
| HsDis(HotSpot disassembler) | JIT生成代碼反生成匯編語句,可以用于分析機(jī)器底層時(shí)怎么理解執(zhí)行我們的java語句。[HSDIS安裝執(zhí)行參考] |
其實(shí)jdk提供了很多監(jiān)控JVM運(yùn)行狀態(tài)的接口,市場上大部分線上排除工具、分析工具都是基于Instrumentation和Attach相關(guān)接口實(shí)現(xiàn)的。
基于Instrumentation可以用獨(dú)立于應(yīng)用程序之外的代理(agent)程序來監(jiān)測和協(xié)助運(yùn)行在JVM上的應(yīng)用程序。這種監(jiān)測和協(xié)助包括但不限于獲取JVM運(yùn)行時(shí)狀態(tài),替換和修改類定義等。 Instrumentation 的最大作用就是類定義的動(dòng)態(tài)改變和操作。Instrumentation結(jié)合字節(jié)碼編程可以無侵入的實(shí)現(xiàn)線上java服務(wù)器的監(jiān)控。
Attach Api家族的成員非常的少。這里我們只關(guān)注2個(gè)類,”VirtualMachine” and “AttachProvider”,AttachProvider 的實(shí)現(xiàn)是針對不同的操作來使用的。正如他的名字提到的, AttachProvider針對每種不同的操作系統(tǒng)提供(provide)一個(gè)可以訪問的 VirtualMachine的入口。
關(guān)于如何利用Instrumentation和Attach接口實(shí)現(xiàn)JVM虛擬機(jī)監(jiān)控以及在線排查工具的實(shí)現(xiàn),我后面會(huì)有單獨(dú)的文章剖析。
Java線上問題分析
線上問題是每個(gè)程序員在開發(fā)過程中不可避免的,線上問題在任何公司都存在,我們能做的只是降低出現(xiàn)的概率和快速定位解決問題。開發(fā)者對線上發(fā)布必須要有敬畏心,同時(shí)也不要怕遇到線上問題。我們總是在發(fā)現(xiàn)bug,解決bug中成長的。
我個(gè)人將線上問題可以分為以下四類:
- 網(wǎng)絡(luò)相關(guān)類
- 應(yīng)用性能類
- 機(jī)器性能類
- 應(yīng)用邏輯類
網(wǎng)絡(luò)相關(guān)異常
當(dāng)我們從系統(tǒng)日志中發(fā)現(xiàn)SocketException、ConnectException、SoketTimeoutException、UnknownHostException、BindException等與網(wǎng)絡(luò)相關(guān)異常時(shí),先通過ping或者telnet(或者通過nc –v {ip} {port})等工具檢測以下相應(yīng)的ip端口是否通。這類問題我們一般找運(yùn)維配置相關(guān)環(huán)境。網(wǎng)絡(luò)相關(guān)異常一般跟Java虛擬機(jī)無關(guān),這里我不再深入分析。
應(yīng)用性能類
應(yīng)用性能相關(guān)的異常又可以分為以下四類,我們逐一分析:
- 運(yùn)行類異常
- 應(yīng)用沒響應(yīng)
- 調(diào)用超時(shí)
- 內(nèi)存溢出
運(yùn)行類異常
現(xiàn)象:當(dāng)應(yīng)用日志中出現(xiàn)NoSuchMethodException、ClassNotFoundException、NoClassDefFoundError、ClassCastException等相關(guān)異常時(shí)。
常見原因:
1)經(jīng)常遇到的包沖突
2)Java ClassLoader機(jī)制引起的加載順序問題
排查方法:
1)加載順序:在應(yīng)用啟動(dòng)的Vm參數(shù)中添加-XX:+TraceClassLoading 查看應(yīng)用啟動(dòng)加載的jar包信息
2)包沖突:通過mvn dependency:tree 打印依賴樹
應(yīng)用沒響應(yīng)
現(xiàn)象:http返回499、502、504等異常碼
常見原因:
1)java進(jìn)程退出
2)資源被耗光(CPU、內(nèi)存,這種后面單獨(dú)說)
3)死鎖
4)處理線程池耗光
排查方法:
1)死鎖:通過jstack –l 打印當(dāng)前jvm中的所有堆棧信息,查看”wating”狀態(tài)的線程是否存在“當(dāng)前線程locking的資源正式另一個(gè)線程wating的資源”的環(huán)形等待
2)處理線程池耗光:通過jstack –l查看相關(guān)線程數(shù)
3)java進(jìn)程退出:jps或者ps aux|grep “java”查看有沒有相關(guān)進(jìn)程
調(diào)用超時(shí)
現(xiàn)象:業(yè)務(wù)日志各種TimeoutException異常
常見原因:
1)服務(wù)端響應(yīng)慢
2)調(diào)用端或者服務(wù)端存在FullGC
3)調(diào)用端或者服務(wù)端load比較高(后面單獨(dú)說)
4)網(wǎng)絡(luò)問題(參照之前的方案)
排查方法:
先通過公司的服務(wù)鏈路監(jiān)控查看相應(yīng)調(diào)用的調(diào)用鏈路耗時(shí),找到異常的服務(wù)。再登上對應(yīng)應(yīng)用的服務(wù)器查看機(jī)器的負(fù)載信息和服務(wù)相應(yīng)的GC日志。如果服務(wù)器load比較高,需要查看服務(wù)器IO、CPU、丟包率等更細(xì)的指標(biāo)定為出是哪項(xiàng)資源存在瓶頸,結(jié)合服務(wù)器流量、操作行為(訪問磁盤頻率、訪問文件大小)定為出具體問題。如果GC比較頻繁,那就dump一份內(nèi)存,分析一下是不是存在內(nèi)存泄漏或者大量復(fù)雜對象等原因。
內(nèi)存溢出
現(xiàn)象:業(yè)務(wù)日志出現(xiàn)java.lang.OutOfMemoryError異常,OOM后面可能跟著
1)GC overhead limit exceeded java heap space(堆溢出)
2)Unable to create new native thread(無法創(chuàng)建線程)
3)PermGen Space(永生代異常)
4)Direct buffer memory(直接內(nèi)存溢出)
常見原因:
1)Java Heap分配不出需要的內(nèi)存,存在內(nèi)存泄漏
2)線程數(shù)超過了ulimit限制或者線程數(shù)超過了kernel.pid_max
3)加載的類、常量等信息超過JVM中永生代的內(nèi)存限制
4)ByteBuffer.allocateDirect申請的內(nèi)存塊超過 –Xmx的大小
排查方法:
1)堆溢出:通過-XX:+HeapDumpOnOutOfMemeryError拿到內(nèi)存dump文件或者jmap –dump:file=<文件名>,format=b pid 拿到HeapDump文件,然后通過MAT 相關(guān)工具分析上面得到的HeapDump文件
2)無法創(chuàng)建線程:ps -eLf|grep java –c 查看當(dāng)前所有的線程數(shù) 和 cat /proc/[pid]/limits 查看某個(gè)進(jìn)程的資源限制
3)永生代異常:調(diào)大PermSize
4)直接內(nèi)存:通過-XX:MaxDirectMemorySize 調(diào)節(jié)大小
機(jī)器性能類異常
服務(wù)器性能又體現(xiàn)在CPU、內(nèi)存、磁盤IO三塊。下面逐個(gè)分析
CPU核心指標(biāo)
us :用戶空間占用CPU百分比</br>
sy : 內(nèi)核空間占用CPU百分比
wa :等待輸入輸出的CPU時(shí)間百分比
load: 綜合指標(biāo),指的是運(yùn)行隊(duì)列(run-queue)的長度(等待進(jìn)程的數(shù)目 + 運(yùn)行進(jìn)程的數(shù)目)
應(yīng)用內(nèi)存核心指標(biāo)
VIRT: 當(dāng)前進(jìn)程對虛擬內(nèi)存使用量。
RES:當(dāng)前進(jìn)程的物理內(nèi)存使用量。
SHR:當(dāng)前進(jìn)程的共享內(nèi)存使用量。
磁盤IO
r/s:每秒發(fā)送到設(shè)備的讀入請求數(shù).</br>
w/s:每秒發(fā)送到設(shè)備的寫入請求數(shù).</br>
rsec/s:每秒從設(shè)備讀入的扇區(qū)數(shù).</br>
wsec/s:每秒向設(shè)備寫入的扇區(qū)數(shù).
await:I/O請求平均執(zhí)行時(shí)間,包括發(fā)送請求和執(zhí)行的時(shí)間,單位是毫秒.
%util:在I/O請求發(fā)送到設(shè)備期間,占用CPU時(shí)間的百分比,用于顯示設(shè)備的帶寬利用率。當(dāng)這個(gè)值接近100%時(shí),表示設(shè)備帶寬已經(jīng)占滿.
常見問題
us高:代碼中出現(xiàn)非常耗CPU的操作或者出現(xiàn)頻繁的FullGC
sy高:鎖競爭激烈,線程切換頻繁
iowait高:io讀寫操作頻繁
load高:一般根據(jù)cpu數(shù)量去判斷,Load值大于CPU的數(shù)量才算高。load是可以理解為一個(gè)綜合指標(biāo),一般伴隨著CPU、IO異常一起出現(xiàn)。滿足以下條件就會(huì)進(jìn)入CPU執(zhí)行等待隊(duì)列,就會(huì)被load值統(tǒng)計(jì)進(jìn)去:1)它沒有在等待I/O操作的結(jié)果;2)它沒有主動(dòng)進(jìn)入等待狀態(tài)(也就是沒有調(diào)用’wait’);3)沒有被停止(例如:等待終止)
查看這些參數(shù)的命令
top (-H):top可以實(shí)時(shí)的觀察cpu的指標(biāo)狀況,尤其是每個(gè)core的指標(biāo)狀況,可以更有效的來幫助解決問題,-H則有助于看是什么線程造成的CPU消耗,這對解決一些簡單的耗CPU的問題會(huì)有很大幫助。
Sar:sar有助于查看歷史指標(biāo)數(shù)據(jù),除了CPU外,其他內(nèi)存,磁盤,網(wǎng)絡(luò)等等各種指標(biāo)都可以查看,畢竟大部分時(shí)候問題都發(fā)生在過去,所以翻歷史記錄非常重要。
PS:所有的問題都需要具體分析,但是問題分析的前提是我們要知道各個(gè)指標(biāo)的確切定義,不然容易丟失關(guān)鍵信息而一直無法發(fā)現(xiàn)真正原因。
業(yè)務(wù)邏輯異常
其實(shí)我們遇到90%以上的線上問題都是邏輯問題,邏輯問題在本地我們可以通過工具一行一行debug確定問題。本地環(huán)境和線上環(huán)境一般情況下不互通,需要跳板機(jī)中轉(zhuǎn),同時(shí)遠(yuǎn)程DEBUG很有可能將其他正常的業(yè)務(wù)請求攔下,影響其他用戶的使用。推薦一款很好用的在線排查工具grace,grace文檔的使用說明已經(jīng)很詳細(xì),我不再累述,在線排查的原理我后面會(huì)有單獨(dú)的文章分析。
線上故障思考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
總結(jié)
以上是生活随笔為你收集整理的JVM内存管理机制线上问题排查的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【跃迁之路】【497天】程序员高效学习方
- 下一篇: 几个基于jvm 的微服务框架