一、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檔案内容
<code>1)觸發緻命錯誤的操作異常或者信号</code>
<code>2)版本和配置資訊</code>
<code>3)觸發緻命異常的線程詳細資訊和線程棧</code>
<code>4)目前運作的線程清單和它們的狀态</code>
<code>5)堆的總括資訊</code>
<code>6)加載的本地庫</code>
<code>7)指令行參數</code>
<code>8)環境變量</code>
<code>9)OS的CPU資訊</code>
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
由衷感謝:海水同學支援。
<b></b>
<b>本文轉自MT_IT51CTO部落格,原文連結:http://blog.51cto.com/hmtk520/2067043</b><b>,如需轉載請自行聯系原作者</b>