Java常見問題分析

一、JVM簡介
1.JVM內存模型
實際佔用內存大小:-XX:MaxPermSize + -Xmx + -Xss + -XX:MaxDirectMemorySize
如圖一:
1.png

主要分爲:非堆內存+堆內存+棧內存+堆外內存
JVM主要管理兩種類型的內存:堆和非堆。簡單來說堆就是Java代碼可及的內存,是留給開發人員使用的;非堆就是JVM留給自己用的
在JVM中堆之外的內存稱爲非堆內存(Non-heap memory)。
Java虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。堆是在Java虛擬機啓動時創建的。
堆外內存:DirectMemory是java nio引入的,直接以native的方式分配內存,不受jvm管理。這種方式是爲了提高網絡和文件IO的效率,避免多餘的內存拷貝而出現的。
棧:每個線程執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括局部變量區和操作數棧,用於存放此次方法調用過程中的臨時變量、參數和中間結果。

2.堆內存分三代
共劃分爲:年輕代(Young Generation)、年老代(old generation tenured)和持久代(Permanent Generation)。
持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。
年輕代:[Eden/Survisor/Survisor]
    所有新生成的對象首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的對象。
    年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的對象,將被複制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。同時,根據程序需要,Survivor區是可以配置爲多個的(多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。
年老代:
    在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。
持久代:
    用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=<N>進行設置。
新生代和老年代都是堆內存空間
堆的內存模型:
[Eden|from|to]-[old]
\__young____/--\old/
默認的Edem:from:to=8:1:1(可以通過參數–XX:SurvivorRatio來設定),即:Eden=8/10的新生代(young)空間大小,from=to=1/10 的新生代空間大小。
新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。其中,新生代 ( Young )

3.GC
Scavenge GC
    一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因爲大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裏需要使用速度快、效率高的算法,使Eden去能儘快空閒出來。
    新生代通常存活時間較短,因此基於複製算法來進行回收,所謂複製算法就是掃描出存活的對象,並複製到一塊新的完全未使用的空間中,對應於新生代,就是在Eden和其中一個Survivor,複製到另一個之間Survivor空間中,然後清理掉原來就是在Eden和其中一個Survivor中的對象。新生代採用空閒指針的方式來控制GC觸發,指針保持最後一個分配的對象在新生代區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發GC。當連續分配對象時,對象會逐漸從eden到 survivor,最後到老年代。
Full GC
    對整個堆進行整理,包括Young、Tenured和Perm。Full GC因爲需要對整個對進行回收,所以比Scavenge GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:
    舊生代與新生代不同,對象存活的時間比較長,比較穩定,因此採用標記(Mark)算法來進行回收,所謂標記就是掃描出存活的對象,然後再進行回收未被標記的對象,回收後對用空出的空間要麼進行合併,要麼標記出來便於下次進行分配,總之就是要減少內存碎片帶來的效率損耗。
年老代(Tenured)被寫滿
持久代(Perm)被寫滿
System.gc()被顯示調用
上一次GC之後Heap的各域分配策略動態變化

4.JVM提供的GC方式
JVM提供了串行GC(SerialGC)、並行回收GC(ParallelScavenge)和並行GC(ParNew)
1)串行GC
    在整個掃描和複製過程採用單線程的方式來進行,適用於單CPU、新生代空間較小及對暫停時間要求不是非常高的應用上,是client級別默認的GC方式,可以通過-XX:+UseSerialGC來強制指定
2)並行回收GC
    在整個掃描和複製過程採用多線程的方式來進行,適用於多CPU、對暫停時間要求較短的應用上,是server級別默認採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數
3)並行GC
    與舊生代的併發GC配合使用
如圖二:
2.png

二、java常見問題處理
1.進程異常退出
=======================================
可能的原因:
1)系統OOM Killer //grep kill /var/log/messages,查看kill時對應的內存佔用total-vm,anon-rss,file-rss
2)人爲的kill  //history |grep -i kill
3)代碼代用system.exit() //反查代碼
4)JVM自身bug //DirectMemory 的默認大小是64M,而JDK6之前和JDK6的某些版本的SUN JVM,存在一個BUG,在用-Xmx設定堆空間大小的時候,也設置了DirectMemory的大小。加入設置了-Xmx2048m,那麼jvm最終可分配的內存大小爲4G多一些,是預期的兩倍。
解決方式是設置jvm參數-XX:MaxDirectMemorySize=128m,指定DirectMemory的大小。
5)內存問題    //內存不足,比如申請一個大的對象的時間。不能及時gc
6)native stack溢出導致 //不受jvm控制,但是被java佔用的
致命錯誤出現的時候,JVM生成了hs_err_pid<pid>.log這樣的文件,其中往往包含了虛擬機崩潰原因的重要信息
默認創建在工作目錄:可以結合find -name hs_err_pid*
hs_err_pid<pid>.log文件內容

1)觸發致命錯誤的操作異常或者信號
2)版本和配置信息
3)觸發致命異常的線程詳細信息和線程棧
4)當前運行的線程列表和它們的狀態
5)堆的總括信息
6)加載的本地庫
7)命令行參數
8)環境變量
9)OS的CPU信息

2.OOM
=======================================
1)Java heap space/GC overhead limit exceeded
    dump分析:啓動參數-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=  或者jmap -dump:format=b,file=文件名 [pid]
    使用mat工具分析heapdump
    佔用內存較大代碼優化
    如果內存佔用不多:可能是創建了一個大對象導致,根據日誌分析創建大對象的時間/jstack分析是否存在死循環
2)PermGen space
    調大PermSize
    是否動態加載Groovy腳本
    是否有動態生成類邏輯,比如使用cglib大量動態生成類
3)Direct buffer memory
    默認佔用-Xmx相同的內存 //-XX:MaxDirectMemorySize=1G調整
    網絡通信使用Netty但是未限流
    分析代碼中是否使用DirectyBuffer未合理控制
4)java.lang.StackOverflowError
    調小-Xss使每個線程棧的內存佔用減小 //設置每個線程的堆棧大小
    調小-Xmx,給棧更多的內存空間
    分析代碼中是否存在不合理的遞歸
5)request bytes for Out of swap space
    地址空間不夠 //64bitos
    物理內存不夠:jmap -histo:live pid ,如果內存明顯減少,說明是directbuffer問題,通過-XX:MaxDirectMemorySize設定
    btrace Inflater/Deflater
6)unable to create new native thread
    ulimit -a //vim /etc/security/limits.conf添加
    * soft noproc 11000
    * hard noproc 11000
    * soft nofile 5000  //修改限制
    * hard nofile 5000  //修改限制
/proc/sys/kernel/pid_max 操作系統線程數限制
/proc/sys/vm/max_map_count 單進程mmap的限制會影響
/proc/sys/kernel/thread-max
/proc/sys/vm/max_map_count
max_user_process(ulimit -u)

7)Map failed
如圖三:
3.png

3.CPU過高
=======================================
基本命令:top,vmstat,mpstat,sar,tsar
us高:用戶進程消耗的CPU時間多
    原因:full gc,CMS gc,代碼死循環,整體消耗CPU多等
    方案:查看gc.log ;jstat -gcutil [pid] //https://github.com/oldratlee/useful-scripts/blob/master/show-busy-javathreads.sh
sy高:內核消耗的CPU時間多
    原因:鎖競爭激烈,線程主動切換頻繁
    方案:jstack查看是否有鎖,或者是否是線程切換頻繁。//修改爲無鎖結構,線程切換頻繁改爲通知機制
        btrace ConditionObject.awaitNanos 是否存在很小值,最好是ms級別
wa高:等待IO的CPU時間多,隨機IO太多或者磁盤性能問題
    原因:io讀寫頻繁
    方案:iostat,iotop,lsof//增加緩存,同步改爲異步,隨機寫入改爲順序寫

4.應用無響應
=======================================
CPU高
OOM
死鎖     jstack -l 查看對應死鎖的線程 //去掉死鎖,
線程池滿    //增大線程池,減少耗時

5.環境變量異常
=======================================
時區錯誤/變量錯誤/編碼方式錯誤
解決方案:
    jinfo 查看具體的啓動參數

6.調用超時    
=======================================
服務端慢/服務端或調用端gc/服務端或調用端CPU高/大對象序列化慢/網絡問題,丟包

=======================================
三、案例分析
案例一:"PermGen space"
java.lang.OutOfMemoryError: PermGen space
Exception in thread "http-bio-17788-exec-75"
明顯可以看出是老年代的內存溢出,說明在容器下的靜態文件過多,比如編譯的字節碼,jsp編譯成servlet,或者jar包。
解決此問題,修改jvm的參數 permsize即可,permsize初始默認爲64m。

-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
-vmargs 說明後面是VM的參數,所以後面的其實都是JVM的參數了
-Xms128m JVM初始分配的堆內存
-Xmx512m JVM最大允許分配的堆內存,按需分配
-XX:PermSize=64M JVM初始分配的非堆內存
-XX:MaxPermSize=128M JVM最大允許分配的非堆內存,按需分配

http://makaidong.com/gsycwh/1/147114_9379890.html
由衷感謝:海水同學支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章