http://blog.csdn.net/renfufei/article/details/76350794
每個(gè)Java程序都只能使用一定量的內(nèi)存, 這種限制是由JVM的啟動(dòng)參數(shù)決定的畏梆。而更復(fù)雜的情況在于, Java程序的內(nèi)存分為兩部分: 堆內(nèi)存(Heap space)和 永久代(Permanent Generation, 簡稱 Permgen):
這兩個(gè)區(qū)域的最大內(nèi)存大小, 由JVM啟動(dòng)參數(shù)?-Xmx?和?-XX:MaxPermSize?指定. 如果沒有明確指定, 則根據(jù)平臺(tái)類型(OS版本+ JVM版本)和物理內(nèi)存的大小來確定左冬。
假如在創(chuàng)建新的對(duì)象時(shí), 堆內(nèi)存中的空間不足以存放新創(chuàng)建的對(duì)象, 就會(huì)引發(fā)java.lang.OutOfMemoryError: Java heap space?錯(cuò)誤寥袭。
不管機(jī)器上還沒有空閑的物理內(nèi)存, 只要堆內(nèi)存使用量達(dá)到最大內(nèi)存限制,就會(huì)拋出?java.lang.OutOfMemoryError: Java heap space?錯(cuò)誤。
產(chǎn)生?java.lang.OutOfMemoryError: Java heap space?錯(cuò)誤的原因, 很多時(shí)候, 就類似于將 XXL 號(hào)的對(duì)象,往 S 號(hào)的 Java heap space 里面塞础锐。其實(shí)清楚了原因, 就很容易解決對(duì)不對(duì)? 只要增加堆內(nèi)存的大小, 程序就能正常運(yùn)行. 另外還有一些比較復(fù)雜的情況, 主要是由代碼問題導(dǎo)致的:
超出預(yù)期的訪問量/數(shù)據(jù)量。 應(yīng)用系統(tǒng)設(shè)計(jì)時(shí),一般是有 “容量” 定義的, 部署這么多機(jī)器, 用來處理一定量的數(shù)據(jù)/業(yè)務(wù)漾根。 如果訪問量突然飆升, 超過預(yù)期的閾值, 類似于時(shí)間坐標(biāo)系中針尖形狀的圖譜, 那么在峰值所在的時(shí)間段, 程序很可能就會(huì)卡死、并觸發(fā)?java.lang.OutOfMemoryError: Java heap space?錯(cuò)誤到踏。
內(nèi)存泄露(Memory leak). 這也是一種經(jīng)常出現(xiàn)的情形。由于代碼中的某些錯(cuò)誤, 導(dǎo)致系統(tǒng)占用的內(nèi)存越來越多. 如果某個(gè)方法/某段代碼存在內(nèi)存泄漏的, 每執(zhí)行一次, 就會(huì)(有更多的垃圾對(duì)象)占用更多的內(nèi)存. 隨著運(yùn)行時(shí)間的推移, 泄漏的對(duì)象耗光了堆中的所有內(nèi)存, 那么?java.lang.OutOfMemoryError: Java heap space?錯(cuò)誤就爆發(fā)了尚猿。
以下代碼非常簡單, 程序試圖分配容量為 2M 的 int 數(shù)組. 如果指定啟動(dòng)參數(shù)?-Xmx12m, 那么就會(huì)發(fā)生?java.lang.OutOfMemoryError: Java heap space?錯(cuò)誤窝稿。而只要將參數(shù)稍微修改一下, 變成?-Xmx13m, 錯(cuò)誤就不再發(fā)生。
publicclassOOM {staticfinalintSIZE=2*1024*1024;publicstaticvoidmain(String[] a) {int[] i =newint[SIZE];? ? }}
1
2
3
4
5
6
這個(gè)示例更真實(shí)一些凿掂。在Java中, 創(chuàng)建一個(gè)新對(duì)象時(shí), 例如?Integer num = new Integer(5);?, 并不需要手動(dòng)分配內(nèi)存伴榔。因?yàn)?JVM 自動(dòng)封裝并處理了內(nèi)存分配. 在程序執(zhí)行過程中, JVM 會(huì)在必要時(shí)檢查內(nèi)存中還有哪些對(duì)象仍在使用, 而不再使用的那些對(duì)象則會(huì)被丟棄, 并將其占用的內(nèi)存回收和重用。這個(gè)過程稱為?垃圾收集. JVM中負(fù)責(zé)垃圾回收的模塊叫做?垃圾收集器(GC)庄萎。
Java的自動(dòng)內(nèi)存管理依賴?GC, GC會(huì)一遍又一遍地掃描內(nèi)存區(qū)域, 將不使用的對(duì)象刪除. 簡單來說,?Java中的內(nèi)存泄漏, 就是那些邏輯上不再使用的對(duì)象, 卻沒有被?垃圾收集程序?給干掉. 從而導(dǎo)致垃圾對(duì)象繼續(xù)占用堆內(nèi)存中, 逐漸堆積, 最后造成?java.lang.OutOfMemoryError: Java heap space?錯(cuò)誤踪少。
很容易寫個(gè)BUG程序, 來模擬內(nèi)存泄漏:
import java.util.*;publicclassKeylessEntry {staticclass Key {? ? ? ? Integer id;? ? ? ? Key(Integer id) {this.id = id;? ? ? ? }? ? ? ? @OverridepublicinthashCode() {returnid.hashCode();? ? ? ? }? ? }publicstaticvoidmain(String[] args) {? ? ? ? Map m =newHashMap();while(true){for(inti =0; i <10000; i++){if(!m.containsKey(newKey(i))){? ? ? ? ? ? ? m.put(newKey(i),"Number:"+ i);? ? ? ? ? }? ? ? ? }? ? ? ? System.out.println("m.size()="+ m.size());? ? ? ? }? ? }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
粗略一看, 可能覺得沒什么問題, 因?yàn)檫@最多緩存 10000 個(gè)元素嘛! 但仔細(xì)審查就會(huì)發(fā)現(xiàn),?Key?這個(gè)類只重寫了?hashCode()?方法, 卻沒有重寫?equals()?方法, 于是就會(huì)一直往 HashMap 中添加更多的 Key。
請(qǐng)參考:?Java中hashCode與equals方法的約定及重寫原則
隨著時(shí)間推移, “cached” 的對(duì)象會(huì)越來越多. 當(dāng)泄漏的對(duì)象占滿了所有的堆內(nèi)存,?GC?又清理不了, 就會(huì)拋出?java.lang.OutOfMemoryError:Java heap space?錯(cuò)誤糠涛。
解決辦法很簡單, 在?Key?類中恰當(dāng)?shù)貙?shí)現(xiàn)?equals()?方法即可:
@Overridepublicbooleanequals(Object o) {booleanresponse =false;if(oinstanceofKey) {? ? ? response = (((Key)o).id).equals(this.id);? ? }returnresponse;}
1
2
3
4
5
6
7
8
說實(shí)話, 在尋找真正的內(nèi)存泄漏原因時(shí), 你可能會(huì)死掉很多很多的腦細(xì)胞援奢。
譯者曾經(jīng)碰到過這樣一種場景:
為了輕易地兼容從 Struts2 遷移到 SpringMVC 的代碼, 在 Controller 中直接獲取 request.
所以在?ControllerBase?類中通過?ThreadLocal?緩存了當(dāng)前線程所持有的 request 對(duì)象:
publicabstractclassControllerBase {privatestaticThreadLocal requestThreadLocal =newThreadLocal();publicstaticHttpServletRequestgetRequest(){returnrequestThreadLocal.get();? ? }publicstaticvoidsetRequest(HttpServletRequest request){if(null== request){? ? ? ? requestThreadLocal.remove();return;? ? ? ? }? ? ? ? requestThreadLocal.set(request);? ? }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
然后在 SpringMVC的攔截器(Interceptor)實(shí)現(xiàn)類中, 在?preHandle?方法里, 將 request 對(duì)象保存到 ThreadLocal 中:
/**
* 登錄攔截器
*/publicclassLoginCheckInterceptorimplementsHandlerInterceptor{privateList excludeList =newArrayList();publicvoidsetExcludeList(List excludeList) {this.excludeList = excludeList;? ? }privatebooleanvalidURI(HttpServletRequest request){// 如果在排除列表中String uri = request.getRequestURI();? ? ? ? Iterator iterator = excludeList.iterator();while(iterator.hasNext()) {? ? ? ? String exURI = iterator.next();if(null!= exURI && uri.contains(exURI)){returntrue;? ? ? ? }? ? ? ? }// 可以進(jìn)行登錄和權(quán)限之類的判斷LoginUser user = ControllerBase.getLoginUser(request);if(null!= user){returntrue;? ? ? ? }// 未登錄,不允許returnfalse;? ? }privatevoidinitRequestThreadLocal(HttpServletRequest request){? ? ? ? ControllerBase.setRequest(request);? ? ? ? request.setAttribute("basePath", ControllerBase.basePathLessSlash(request));? ? }privatevoidremoveRequestThreadLocal(){? ? ? ? ControllerBase.setRequest(null);? ? }@OverridepublicbooleanpreHandle(HttpServletRequest request,? ? ? ? HttpServletResponse response, Object handler)throwsException {? ? ? ? initRequestThreadLocal(request);// 如果不允許操作,則返回false即可if(false== validURI(request)) {// 此處拋出異常,允許進(jìn)行異常統(tǒng)一處理thrownewNeedLoginException();? ? ? ? }returntrue;? ? }@OverridepublicvoidpostHandle(HttpServletRequest request,? ? ? ? HttpServletResponse response, Object handler, ModelAndView modelAndView)throwsException {? ? ? ? removeRequestThreadLocal();? ? }@OverridepublicvoidafterCompletion(HttpServletRequest request,? ? ? ? HttpServletResponse response, Object handler, Exception ex)throwsException {? ? ? ? removeRequestThreadLocal();? ? }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
在?postHandle?和?afterCompletion?方法中, 清理 ThreadLocal 中的 request 對(duì)象。
但在實(shí)際使用過程中, 業(yè)務(wù)開發(fā)人員將一個(gè)很大的對(duì)象(如占用內(nèi)存200MB左右的List)設(shè)置為 request 的 Attributes忍捡, 傳遞到 JSP 中集漾。
JSP代碼中可能發(fā)生了異常, 則SpringMVC的postHandle?和?afterCompletion?方法不會(huì)被執(zhí)行。
Tomcat 中的線程調(diào)度, 可能會(huì)一直調(diào)度不到那個(gè)拋出了異常的線程, 于是 ThreadLocal 一直 hold 住 request砸脊。 隨著運(yùn)行時(shí)間的推移,把可用內(nèi)存占滿, 一直在執(zhí)行 Full GC, 系統(tǒng)直接卡死具篇。
后續(xù)的修正: 通過 Filter, 在 finally 語句塊中清理 ThreadLocal。
@WebFilter(value="/*", asyncSupported=true)publicclassClearRequestCacheFilterimplementsFilter{@OverridepublicvoiddoFilter(ServletRequest request, ServletResponse response, FilterChain chain)throwsIOException,? ? ? ? ? ? ServletException {? ? ? ? clearControllerBaseThreadLocal();try{? ? ? ? ? ? chain.doFilter(request, response);? ? ? ? }finally{? ? ? ? ? ? clearControllerBaseThreadLocal();? ? ? ? }? ? }privatevoidclearControllerBaseThreadLocal() {? ? ? ? ControllerBase.setRequest(null);? ? }@Overridepublicvoidinit(FilterConfig filterConfig)throwsServletException {}@Overridepublicvoiddestroy() {}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
教訓(xùn)是:可以使用 ThreadLocal, 但必須有受控制的釋放措施脓规、一般就是?try-finally?的代碼形式栽连。
說明:?SpringMVC 的 Controller 中, 其實(shí)可以通過?@Autowired?注入 request, 實(shí)際注入的是一個(gè)?HttpServletRequestWrapper對(duì)象, 執(zhí)行時(shí)也是通過 ThreadLocal 機(jī)制調(diào)用當(dāng)前的 request。
常規(guī)方式: 直接在controller方法中接收 request 參數(shù)即可侨舆。
如果設(shè)置的最大內(nèi)存不滿足程序的正常運(yùn)行, 只需要增大堆內(nèi)存即可, 配置參數(shù)可以參考下文秒紧。
但很多情況下, 增加堆內(nèi)存空間并不能解決問題。比如存在內(nèi)存泄漏, 增加堆內(nèi)存只會(huì)推遲?java.lang.OutOfMemoryError: Java heap space?錯(cuò)誤的觸發(fā)時(shí)間挨下。
當(dāng)然, 增大堆內(nèi)存, 可能會(huì)增加?GC pauses?的時(shí)間, 從而影響程序的?吞吐量或延遲熔恢。
要從根本上解決問題, 則需要排查分配內(nèi)存的代碼. 簡單來說, 需要解決這些問題:
哪類對(duì)象占用了最多內(nèi)存?
這些對(duì)象是在哪部分代碼中分配的臭笆。
要搞清這一點(diǎn), 可能需要好幾天時(shí)間叙淌。下面是大致的流程:
獲得在生產(chǎn)服務(wù)器上執(zhí)行堆轉(zhuǎn)儲(chǔ)(heap dump)的權(quán)限〕钇蹋“轉(zhuǎn)儲(chǔ)”(Dump)是堆內(nèi)存的快照, 稍后可以用于內(nèi)存分析. 這些快照中可能含有機(jī)密信息, 例如密碼鹰霍、信用卡賬號(hào)等, 所以有時(shí)候, 由于企業(yè)的安全限制, 要獲得生產(chǎn)環(huán)境的堆轉(zhuǎn)儲(chǔ)并不容易。
在適當(dāng)?shù)臅r(shí)間執(zhí)行堆轉(zhuǎn)儲(chǔ)茵乱。一般來說,內(nèi)存分析需要比對(duì)多個(gè)堆轉(zhuǎn)儲(chǔ)文件, 假如獲取的時(shí)機(jī)不對(duì), 那就可能是一個(gè)“廢”的快照. 另外, 每次執(zhí)行堆轉(zhuǎn)儲(chǔ), 都會(huì)對(duì)JVM進(jìn)行“凍結(jié)”, 所以生產(chǎn)環(huán)境中,也不能執(zhí)行太多的Dump操作,否則系統(tǒng)緩慢或者卡死,你的麻煩就大了茂洒。
用另一臺(tái)機(jī)器來加載Dump文件。一般來說, 如果出問題的JVM內(nèi)存是8GB, 那么分析 Heap Dump 的機(jī)器內(nèi)存需要大于 8GB. 打開轉(zhuǎn)儲(chǔ)分析軟件(我們推薦Eclipse MAT?, 當(dāng)然你也可以使用其他工具)瓶竭。
檢測快照中占用內(nèi)存最大的 GC roots督勺。詳情請(qǐng)參考:?Solving OutOfMemoryError (part 6) – Dump is not a waste渠羞。 這對(duì)新手來說可能有點(diǎn)困難, 但這也會(huì)加深你對(duì)堆內(nèi)存結(jié)構(gòu)以及navigation機(jī)制的理解。
接下來, 找出可能會(huì)分配大量對(duì)象的代碼. 如果對(duì)整個(gè)系統(tǒng)非常熟悉, 可能很快就能定位了智哀。
打個(gè)廣告, 我們推薦?Plumbr, the only Java monitoring solution with automatic root cause detection次询。 Plumbr 能捕獲所有的java.lang.OutOfMemoryError?, 并找出其他的性能問題, 例如最消耗內(nèi)存的數(shù)據(jù)結(jié)構(gòu)等等。
Plumbr 在后臺(tái)負(fù)責(zé)收集數(shù)據(jù) —— 包括堆內(nèi)存使用情況(只統(tǒng)計(jì)對(duì)象分布圖, 不涉及實(shí)際數(shù)據(jù)),以及在堆轉(zhuǎn)儲(chǔ)中不容易發(fā)現(xiàn)的各種問題瓷叫。 如果發(fā)生?java.lang.OutOfMemoryError?, 還能在不停機(jī)的情況下, 做必要的數(shù)據(jù)處理. 下面是Plumbr 對(duì)一個(gè)?java.lang.OutOfMemoryError?的提醒:
強(qiáng)大吧, 不需要其他工具和分析, 就能直接看到:
哪類對(duì)象占用了最多的內(nèi)存(此處是 271 個(gè)?com.example.map.impl.PartitionContainer?實(shí)例, 消耗了 173MB 內(nèi)存, 而堆內(nèi)存只有 248MB)
這些對(duì)象在何處創(chuàng)建(大部分是在?MetricManagerImpl?類中,第304行處)
當(dāng)前是誰在引用這些對(duì)象(從 GC root 開始的完整引用鏈)
得知這些信息, 就可以定位到問題的根源, 例如是當(dāng)?shù)鼐啍?shù)據(jù)結(jié)構(gòu)/模型, 只占用必要的內(nèi)存即可屯吊。
當(dāng)然, 根據(jù)內(nèi)存分析的結(jié)果, 以及Plumbr生成的報(bào)告, 如果發(fā)現(xiàn)對(duì)象占用的內(nèi)存很合理, 也不需要修改源代碼的話, 那就增大堆內(nèi)存吧。在這種情況下,修改JVM啟動(dòng)參數(shù), (按比例)增加下面的值:
-Xmx1024m
這里配置Java堆內(nèi)存最大為?1024MB赞辩〈蒲浚可以使用?g/G?表示 GB,?m/M?代表 MB,?k/K?表示 KB.
下面的這些形式都是等價(jià)的, 設(shè)置Java堆的最大空間為 1GB:
# 等價(jià)形式: 最大1GB內(nèi)存
java -Xmx1073741824 com.mycompany.MyClass
java -Xmx1048576k com.mycompany.MyClass
java -Xmx1024m com.mycompany.MyClass
java -Xmx1g com.mycompany.MyClass