1. 程式人生 > >Java常見問題分析

Java常見問題分析

垃圾收集 完全 不同 als 生命周期 rac 原來 之間 nano

一、JVM簡介
1.JVM內存模型
實際占用內存大小:-XX:MaxPermSize + -Xmx + -Xss + -XX:MaxDirectMemorySize
如圖一:
技術分享圖片

主要分為:非堆內存+堆內存+棧內存+堆外內存
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配合使用
如圖二:
技術分享圖片

二、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.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
由衷感謝:海水同學支持。

Java常見問題分析