Java內存故障?只是因為你不夠帥!
本文轉載自微信公眾號「小姐姐味道」,作者小姐姐養的狗 。轉載本文請聯系小姐姐味道公眾號。
從小我就對Java有著深厚的感情,算下來有幾十年的Java經驗了。當年的Java還是Sun公司的,我有著多年的Servlet經驗,CURD經驗,在現在已經被自我革新,轉而研究人生的哲學。罷了,不吹了。本文是關于Java故障排查的,屬上篇。
為了保證文章的流暢性,我決定一口氣把它寫完。因為相關方面的培訓做的多了,就不需要在寫的時候參考資料、翻源代碼。掐指一算,本文一個小時沒花掉,但篇幅已經較長了。
長了,那就割斷。本篇就定為內存排查的上篇,主要講一些原理。為什么要講原理?開車還需要了解汽車結構么?
這還真不能相比。
汽車很少壞,出了問題你會花錢給拖車公司、4S店。你還會每年給它買上保險。
反觀Java,三天兩頭出問題,找人解決還找不到人,給錢都不一定能解決問題。能比么?盤點來盤點去,最后只能靠自己。
- 1.內存里都有啥
- 2.操作系統內存
- 3.JVM內存劃分
- 4.一圖解千愁,jvm內存從來沒有這么簡單過!
- 5.為什么會有內存問題
- 6.垃圾回收器
- 7.重要概念GC Roots
- 8.對象的提升
1.內存里都有啥
要想排查內存問題,我們就需要看一下內存里都有啥。我們先來看一下操作系統內存的劃分,然后再來看一下JVM內存的劃分。由于JVM本身是作為一個正常的應用運行在操作系統上的,所以它的行為同時會受到操作系統的限制。
2.操作系統內存
我們首先從操作系統的實現來說起。通常情況下,我們寫了一個C語言程序,編譯后,會發現里面的內存地址是固定的。其實我們的應用程序在編譯之后,這些地址都是虛擬地址。他需要經過一層翻譯之后,才能映射到真正的物理內存,MMU就是負責地址轉換的硬件。
那我們操作系統的可用內存到底是多少呢?它其實是分為兩部分的。一部分是物理內存,指的是我們插的那根內存條;另一部分就是使用磁盤模擬的虛擬內存,在Linux通常稱做swap分區。所以,可用內存 = 物理內存 + 虛擬內存。如果你的系統開了swap,可用內存就比物理內存大。
通過top命令和free命令都可以看到內存的使用情況。
top命令可以看到每一個進程的內存使用情況,我們平常關注的是RES這一列,它代表的是進程實際的內存占用,我們平常在搭建監控系統的時候,監控的也是這個數值。
我們再來看一下free命令的展示。它的展示其實是有一些混亂的,具體的關系可以看上面的圖。通常情況下,free顯示的數值都是比較小的,但這并不等于系統的可用內存就那么一點點。Linux操作系統啟動后,隨著機器的運行,剩余內存會迅速被buffer和cache這些緩沖區和緩存迅速占滿,而這些內存再應用的內存空間不足時,是可以釋放的。可用內存 = free + buffers + cached。
具體每一個區域的內存使用情況,可以通過/proc/meminfo進行查看的。
- # cat /proc/meminfo
- MemTotal: 3881692 kB
- MemFree: 249248 kB
- MemAvailable: 1510048 kB
- Buffers: 92384 kB
- Cached: 1340716 kB
- 40+ more ...
3.JVM內存劃分
接下來,我們才來看一下JVM的內存區域劃分。
在JVM中,最大的內存區域就是堆,我們平常創建的大部分對象,都會存放在這里。所謂的垃圾回收,也主要針對的是這一部分。
多本JVM書籍描述:JVM中,除了程序計數器,其他區域都是可能溢出的。我們這里依然同意這個結論。下面僅對這些內存區域做簡要的介紹,因為有些知識對我們的內存排查無益。
- 堆:JVM堆中的數據,是共享的,是占用內存最大的一塊區域
- 虛擬機棧:Java虛擬機棧,是基于線程的,用來服務字節碼指令的運行
- 程序計數器:當前線程所執行的字節碼的行號指示器
- 元空間:方法區就在這里,非堆 本地內存:其他的內存占用空間
類比上面這張圖,我們可以歸位一些常用對象的分配位置。不要糾結什么棧上分配逃逸分析,也不用關注棧幀和操作數棧這種雙層的結構,這些小細節對于對象的汪洋大海來說,影響實在是太小。我們關注的內存區域,其實就只有堆內內存和堆外內存兩個概念。
4.一圖解千愁,jvm內存從來沒有這么簡單過!
下面這篇文章,詳細的講解了每個區域。本來想要揉在一塊,但怕突出不了它的重要性。所以開始直接讀原文吧。
5.為什么會有內存問題
統計顯示,我們平常的工作中,OOM/ML問題占比5%左右,平均處理時間卻達到40天左右。這就可以看出這種問題的排查,是非常的困難的。
但讓人無語的是,遇到內存問題,工程師們的現場保護意識往往不足,特別的不足。只知道一個內存溢出的結果,但什么都沒留下。監控沒有,日志沒有,甚至連發生的時間點都不清楚。這樣的問題,鬼才知道原因。
6.垃圾回收器
內存問題有兩種模式,一種是內存溢出,一種是內存泄漏。
- 內存溢出 OutOfMemoryError,簡稱OOM,堆是最常見的情況,堆外內存排查困難。
- 內存泄漏 Memory Leak,簡稱ML,主要指的是分配的內存沒有得到釋放。內存一直在增長,有OOM風險;GC時該回收的回收不掉;或者能夠回收掉但很快又占滿,產生壓力。
內存問題影響也是非常大的,比如下面這三種場景。
- 發生OOM Error,應用停止(最嚴重)
- 頻繁GC,GC時間長,GC線程時間片占用高
- 服務卡頓,請求響應時間變長
說到這卡頓問題,就不得不提一嘴垃圾回收器。
很多同學一看上面的圖,就知道我們要說G1垃圾回收器了,這也是我的推薦。CMS等垃圾回收器,回收時間不可控,如果你有條件,當然要避免使用,CMS也將要在Java14中被移除,我也真心不希望你掌握一些即將過時的經驗。ZGC雖然厲害,但還太新,幾乎沒有人敢吃螃蟹,那剩下的就是G1了。
G1通過三個簡單的配置參數,大部分情況下即可獲取優異的性能,工程師幸福了很多。三個參數如下:
- MaxGCPauseMillis 預定目標,自動調整。
- G1HeapRegionSize 小堆區大小。
- InitiatingHeapOccupancyPercent 堆內存比例閾值,啟動并發標記。
如果你還是不放心,想要了解一下G1的原理,那我們也可以捎帶提上兩嘴。G1其實還是有年輕代老年代的概念的,只不過它的內存是不連續的。
如圖所示,G1將內存切分成大小相等的區域,這些區域叫做小堆區,是垃圾回收的最小單位。以前的垃圾回收器都是整代回收,而G1是部分回收,那就可以根據配置的最小延遲時間合理的選取小堆區的數量,回收過程就顯得智能了很多。
7.重要概念GC Roots
如圖所示,要確定哪些是垃圾,就需要有一種找到垃圾的方法。其實,我們上一句的表述是不正確的。在JVM中,找垃圾的方法和我們理解的正好相反:它是首先找到存活的對象,對存活的對象做標記,然后把其他對象一股腦的回收掉。
JVM在垃圾回收時,關心的是不要把不是垃圾的對象給回收了,而不是把垃圾對象給清理的干干凈凈。
要找到哪些是存活對象,就需要從源頭上追溯。在JVM中,常見的GC Roots就有靜態的成員變量等,比如一個靜態的HashMap。
另外一部分,就是線程所關聯的虛擬機棧和本地方法棧里面的內容。
我們說了這老半天,其實這種追溯方式有一個專有的名詞:可達性分析法。與之類似的還有引用計數法,但由于有環形依賴的問題,所以幾乎沒有回收器使用這種形式。
并不是說只要是和GC Roots有一條聯系(Reference Chain),對象就是存活的,它還與對象的引用級別有關。
- 強引用:屬于最普通最強硬的一種存在,只有在和GC Roots斷絕關系時,才會被消滅掉
- 軟引用:只有在內存不足時,系統則會回收軟引用對象
- 弱引用:當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象
- 虛引用:虛引用主要用來跟蹤對象被垃圾回收的活動
平常情況下,我們使用的對象就是強引用。軟引用和弱引用在一些緩存框架中用的比較廣泛,對象的重要程度也比較弱。
8.對象的提升
大多數垃圾回收器都是分代垃圾回收,我們從上面對G1的描述就能夠看出來。
如圖所示,是典型的分代回收內存模型。對象從年輕代提升到老年代,有四種方式。
- 常規提升,對象夠老。比如從from到to轉了15圈還沒有被回收掉。控制參數就是-XX:MaxTenuringThreshold。這個值在CMS下默認為6,G1下默認為15
- 分配擔保 Survivor 空間不夠,老年代擔保。
- 大對象直接在老年代分配
- 動態對象年齡判定。比如在G1里的TenuringThreshold會隨著堆內對象的分布而變化
對于垃圾回收器的優化,就是要確保盡量多的對象在年輕代里分配,減少對象提升到老年代的可能。雖然這種思想在G1里弱化了許多。
End了解了操作系統的內存里都有啥,又了解了JVM的內存里都有啥,我們就可以淡定縱容的針對于每一種出現問題的情況,進行針對性排查和優化。
文章到這里嘎然而止。下一篇,我們以幾個實際的案例,來看一下Java的內存問題排查的具體過程。