Day704.Tomcat内存溢出的原因分析及调优 -深入拆解 Tomcat Jetty
Tomcat內存溢出的原因分析及調優
Hi,我是阿昌。今天學習記錄的是關于Tomcat內存溢出的原因分析及調優。
作為 Java 程序員,我們幾乎都會碰到 java.lang.OutOfMemoryError 異常,但是你知道有哪些原因可能導致 JVM 拋出 OutOfMemoryError 異常嗎?
JVM 在拋出 java.lang.OutOfMemoryError 時,除了會打印出一行描述信息,還會打印堆棧跟蹤,因此我們可以通過這些信息來找到導致異常的原因。
在尋找原因前,我們先來看看有哪些因素會導致 OutOfMemoryError,其中內存泄漏是導致 OutOfMemoryError 的一個比較常見的原因
一、內存溢出場景及方案
1、java.lang.OutOfMemoryError: Java heap space
JVM 無法在堆中分配對象時,會拋出這個異常,導致這個異常的原因可能有三種:
2、java.lang.OutOfMemoryError: GC overhead limit exceeded
出現這種 OutOfMemoryError 的原因是,垃圾收集器一直在運行,但是 GC 效率很低,比如 Java 進程花費超過 98%的 CPU 時間來進行一次 GC,但是回收的內存少于 2%的 JVM 堆,并且連續 5 次 GC 都是這種情況,就會拋出 OutOfMemoryError。解決辦法是查看 GC 日志或者生成 Heap Dump,確認一下是不是內存泄漏,如果不是內存泄漏可以考慮增加 Java 堆的大小。
當然你還可以通過參數配置來告訴 JVM 無論如何也不要拋出這個異常,方法是配置-XX:-UseGCOverheadLimit,但是我并不推薦這么做,因為這只是延遲了 OutOfMemoryError 的出現。
3、java.lang.OutOfMemoryError: Requested array size exceeds VM limit
拋出這種異常的原因是“請求的數組大小超過 JVM 限制”,應用程序嘗試分配一個超大的數組。
比如應用程序嘗試分配 512MB 的數組,但最大堆大小為 256MB,則將拋出 OutOfMemoryError,并且請求的數組大小超過 VM 限制。
通常這也是一個配置問題(JVM 堆太小),或者是應用程序的一個 Bug,比如程序錯誤地計算了數組的大小,導致嘗試創建一個大小為 1GB 的數組。
4、java.lang.OutOfMemoryError: MetaSpace
如果 JVM 的元空間用盡,則會拋出這個異常。我們知道 JVM 元空間的內存在本地內存中分配,但是它的大小受參數 MaxMetaSpaceSize 的限制。當元空間大小超過 MaxMetaSpaceSize 時,JVM 將拋出帶有 MetaSpace 字樣的 OutOfMemoryError。
解決辦法是加大 MaxMetaSpaceSize 參數的值。
5、java.lang.OutOfMemoryError: Request size bytes for reason. Out of swap space
當本地堆內存分配失敗或者本地內存快要耗盡時,Java HotSpot VM 代碼會拋出這個異常,VM 會觸發“致命錯誤處理機制”,它會生成“致命錯誤”日志文件,其中包含崩潰時線程、進程和操作系統的有用信息。
如果碰到此類型的 OutOfMemoryError,你需要根據 JVM 拋出的錯誤信息來進行診斷;
或者使用操作系統提供的 DTrace 工具來跟蹤系統調用,看看是什么樣的程序代碼在不斷地分配本地內存。
6、java.lang.OutOfMemoryError: Unable to create native threads
因此關鍵在于第四步線程創建失敗,JVM 就會拋出 OutOfMemoryError,那具體有哪些因素會導致線程創建失敗呢?
內存大小限制:我前面提到,Java 創建一個線程需要消耗一定的??臻g,并通過-Xss參數指定。請你注意的是??臻g如果過小,可能會導致 StackOverflowError,尤其是在遞歸調用的情況下;但是棧空間過大會占用過多內存,而對于一個 32 位 Java 應用來說,用戶進程空間是 4GB,內核占用 1GB,那么用戶空間就剩下 3GB,因此它能創建的線程數大致可以通過這個公式算出來:
Max memory(3GB) = [-Xmx] + [-XX:MaxMetaSpaceSize] + number_of_threads * [-Xss]不過對于 64 位的應用,由于虛擬進程空間近乎無限大,因此不會因為線程棧過大而耗盡虛擬地址空間。
但是注意,64 位的 Java 進程能分配的最大內存數仍然受物理內存大小的限制。
ulimit 限制,在 Linux 下執行ulimit -a,你會看到 ulimit 對各種資源的限制。
其中的“max user processes”就是一個進程能創建的最大線程數,我們可以修改這個參數:
 參數sys.kernel.threads-max限制。這個參數限制操作系統全局的線程數,通過下面的命令可以查看它的值。
 這表明當前系統能創建的總的線程是 63752。當然我們調整這個參數,具體辦法是:
在/etc/sysctl.conf配置文件中,加入sys.kernel.threads-max = 999999。
參數sys.kernel.pid_max限制,這個參數表示系統全局的 PID 號數值的限制,每一個線程都有 ID,ID 的值超過這個數,線程就會創建失敗。跟sys.kernel.threads-max參數一樣,我們也可以將sys.kernel.pid_max調大,方法是在/etc/sysctl.conf配置文件中,加入sys.kernel.pid_max = 999999。
對于線程創建失敗的 OutOfMemoryError,除了調整各種參數,我們還需要從程序本身找找原因,看看是否真的需要這么多線程,有可能是程序的 Bug 導致創建過多的線程。
二、內存泄漏定位實戰
我們先創建一個 Web 應用,不斷地 new 新對象放到一個 List 中,來模擬 Web 應用中的內存泄漏。然后通過各種工具來觀察 GC 的行為,最后通過生成 Heap Dump 來找到泄漏點。
內存泄漏模擬程序比較簡單,創建一個 Spring Boot 應用,定義如下所示的類:
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component;import java.util.LinkedList; import java.util.List;@Component public class MemLeaker {private List<Object> objs = new LinkedList<>();@Scheduled(fixedRate = 1000)public void run() {for (int i = 0; i < 50000; i++) {objs.add(new Object());}} }這個程序做的事情就是每隔 1 秒向一個 List 中添加 50000 個對象。
接下來運行并通過工具觀察它的 GC 行為:
運行程序并打開 verbosegc,將 GC 的日志輸出到 gc.log 文件中。
java -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -jar mem-0.0.1-SNAPSHOT.jar使用jstat命令觀察 GC 的過程:
jstat -gc 94223 2000 100094223 是程序的進程 ID,2000 表示每隔 2 秒執行一次,1000 表示持續執行 1000 次。下面是命令的輸出:
其中每一列的含義是:
- S0C:第一個 Survivor 區總的大小;
- S1C:第二個 Survivor 區總的大小;
- S0U:第一個 Survivor 區已使用內存的大小;
- S1U:第二個 Survivor 區已使用內存的大小。
后面的列相信從名字你也能猜出是什么意思了,其中 E 代表 Eden,O 代表 Old,M 代表 Metadata;YGC 表示 Minor GC 的總時間,YGCT 表示 Minor GC 的次數;FGC 表示 Full GC。通過這個工具,你能大概看到各個內存區域的大小、已經 GC 的次數和所花的時間。verbosegc 參數對程序的影響比較小,因此很適合在生產環境現場使用。
通過 GCViewer 工具查看 GC 日志,用 GCViewer 打開第一步產生的 gc.log,會看到這樣的圖:
圖中紅色的線表示年老代占用的內存,你會看到它一直在增加,而黑色的豎線表示一次 Full GC。
你可以看到后期 JVM 在頻繁地 Full GC,但是年老代的內存并沒有降下來,這是典型的內存泄漏的特征。
除了內存泄漏,我們還可以通過 GCViewer 來觀察 Minor GC 和 Full GC 的頻次,已及每次的內存回收量。
為了找到內存泄漏點,我們通過 jmap 工具生成 Heap Dump:
jmap -dump:live,format=b,file=94223.bin 94223用 Eclipse Memory Analyzer 打開 Dump 文件,通過內存泄漏分析,得到這樣一個分析報告:
從報告中可以看到,JVM 內存中有一個長度為 4000 萬的 List,至此我們也就找到了泄漏點。
總結
以上是生活随笔為你收集整理的Day704.Tomcat内存溢出的原因分析及调优 -深入拆解 Tomcat Jetty的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: tornado学习笔记day07-同步与
- 下一篇: 基于dreamweaver软件设计和开发
