作為一個java程序員,大家都應(yīng)該認識JVM。JVM作為java的核心,實在太重要了。而內(nèi)存溢出又是程序員常遇到的錯誤之一,如果你對JVM的原理足夠了解,那么解決這樣的問題就不在是一件困難的事情。
關(guān)于內(nèi)存溢出,一般有下面這八個癥狀,本文將說明引發(fā)特定錯誤的原因,提供了可能導(dǎo)致此類錯誤的代碼示例,并提供了解決方案的修復(fù)準則,希望對做開發(fā)的小伙伴能有一定的幫助。
本篇是第1小篇。
OutOfMemoryError之Java heap space
OutOfMemoryError之GC overhead limit exceeded
OutOfMemoryError之Permgen space
OutOfMemoryError之Metaspace
OutOfMemoryError之Unable to create new native thread
OutOfMemoryError之Out of swap space?
OutOfMemoryError之Requested array size exceeds VM limit
OutOfMemoryError之Kill process or sacrifice child
每個Java程序都只能使用一定量的內(nèi)存, 這種限制是由JVM的啟動參數(shù)決定的。而更復(fù)雜的情況在于, Java程序的內(nèi)存分為兩部分: 堆內(nèi)存(Heap space)和 永久代(Permanent Generation, 簡稱 Permgen):
這兩個區(qū)域的最大內(nèi)存大小, 由JVM啟動參數(shù) -Xmx 和 -XX:MaxPermSize 指定. 如果沒有明確指定, 則根據(jù)平臺類型(OS版本+ JVM版本)和物理內(nèi)存的大小來確定。
假如在創(chuàng)建新的對象時, 堆內(nèi)存中的空間不足以存放新創(chuàng)建的對象, 就會引發(fā)java.lang.OutOfMemoryError: Java heap space 錯誤。
不管機器上還沒有空閑的物理內(nèi)存, 只要堆內(nèi)存使用量達到最大內(nèi)存限制,就會拋出 java.lang.OutOfMemoryError: Java heap space 錯誤。
原因分析
產(chǎn)生 java.lang.OutOfMemoryError: Java heap space 錯誤的原因, 很多時候, 就類似于將 XXL 號的對象,往 S 號的 Java heap space 里面塞。其實清楚了原因, 就很容易解決對不對? 只要增加堆內(nèi)存的大小, 程序就能正常運行. 另外還有一些比較復(fù)雜的情況, 主要是由代碼問題導(dǎo)致的:
- 超出預(yù)期的訪問量/數(shù)據(jù)量。 應(yīng)用系統(tǒng)設(shè)計時,一般是有 “容量” 定義的, 部署這么多機器, 用來處理一定量的數(shù)據(jù)/業(yè)務(wù)。 如果訪問量突然飆升, 超過預(yù)期的閾值, 類似于時間坐標(biāo)系中針尖形狀的圖譜, 那么在峰值所在的時間段, 程序很可能就會卡死、并觸發(fā) java.lang.OutOfMemoryError: Java heap space 錯誤。
- 內(nèi)存泄露(Memory leak). 這也是一種經(jīng)常出現(xiàn)的情形。由于代碼中的某些錯誤, 導(dǎo)致系統(tǒng)占用的內(nèi)存越來越多. 如果某個方法/某段代碼存在內(nèi)存泄漏的, 每執(zhí)行一次, 就會(有更多的垃圾對象)占用更多的內(nèi)存. 隨著運行時間的推移, 泄漏的對象耗光了堆中的所有內(nèi)存, 那么 java.lang.OutOfMemoryError: Java heap space 錯誤就爆發(fā)了。
具體示例
一個非常簡單的示例
以下代碼非常簡單, 程序試圖分配容量為 2M 的 int 數(shù)組. 如果指定啟動參數(shù) -Xmx12m, 那么就會發(fā)生 java.lang.OutOfMemoryError: Java heap space 錯誤。而只要將參數(shù)稍微修改一下, 變成 -Xmx13m, 錯誤就不再發(fā)生。
public class OOM {
static final int SIZE=2*1024*1024;
public static void main(String[] a) {
int[] i = new int[SIZE];
}
}
內(nèi)存泄漏示例
這個示例更真實一些。在Java中, 創(chuàng)建一個新對象時, 例如 Integer num = new Integer(5); , 并不需要手動分配內(nèi)存。因為 JVM 自動封裝并處理了內(nèi)存分配. 在程序執(zhí)行過程中, JVM 會在必要時檢查內(nèi)存中還有哪些對象仍在使用, 而不再使用的那些對象則會被丟棄, 并將其占用的內(nèi)存回收和重用。這個過程稱為 垃圾收集. JVM中負責(zé)垃圾回收的模塊叫做 垃圾收集器(GC)。
Java的自動內(nèi)存管理依賴 GC, GC會一遍又一遍地掃描內(nèi)存區(qū)域, 將不使用的對象刪除. 簡單來說, Java中的內(nèi)存泄漏, 就是那些邏輯上不再使用的對象, 卻沒有被 垃圾收集程序 ****給****干掉. 從而導(dǎo)致垃圾對象繼續(xù)占用堆內(nèi)存中, 逐漸堆積, 最后造成 java.lang.OutOfMemoryError: Java heap space 錯誤。
很容易寫個BUG程序, 來模擬內(nèi)存泄漏:
import java.util.*;
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map m = new HashMap();
while (true){
for (int i = 0; i < 10000; i++){
if (!m.containsKey(new Key(i))){
m.put(new Key(i), "Number:" + i);
}
}
System.out.println("m.size()=" + m.size());
}
}
}
粗略一看, 可能覺得沒什么問題, 因為這最多緩存 10000 個元素嘛! 但仔細審查就會發(fā)現(xiàn), Key 這個類只重寫了 hashCode() 方法, 卻沒有重寫 equals() 方法, 于是就會一直往 HashMap 中添加更多的 Key。
隨著時間推移, “cached” 的對象會越來越多. 當(dāng)泄漏的對象占滿了所有的堆內(nèi)存, GC 又清理不了, 就會拋出 java.lang.OutOfMemoryError:Java heap space 錯誤。
解決辦法很簡單, 在 Key 類中恰當(dāng)?shù)貙崿F(xiàn) equals() 方法即可:
@Override
public boolean equals(Object o) {
boolean response = false;
if (o instanceof Key) {
response = (((Key)o).id).equals(this.id);
}
return response;
}
說實話, 在尋找真正的內(nèi)存泄漏原因時, 你可能會死掉很多很多的腦細胞。
一個SpringMVC中的場景
譯者曾經(jīng)碰到過這樣一種場景:
為了輕易地兼容從 Struts2 遷移到 SpringMVC 的代碼, 在 Controller 中直接獲取 request.
所以在 ControllerBase 類中通過 ThreadLocal 緩存了當(dāng)前線程所持有的 request 對象:
public abstract class ControllerBase {
private static ThreadLocal<HttpServletRequest> requestThreadLocal = new ThreadLocal<HttpServletRequest>();
public static HttpServletRequest getRequest(){
return requestThreadLocal.get();
}
public static void setRequest(HttpServletRequest request){
if(null == request){
requestThreadLocal.remove();
return;
}
requestThreadLocal.set(request);
}
}
然后在 SpringMVC的攔截器(Interceptor)實現(xiàn)類中, 在 preHandle 方法里, 將 request 對象保存到 ThreadLocal 中:
/**
* 登錄攔截器
*/
public class LoginCheckInterceptor implements HandlerInterceptor {
private List<String> excludeList = new ArrayList<String>();
public void setExcludeList(List<String> excludeList) {
this.excludeList = excludeList;
}
private boolean validURI(HttpServletRequest request){
// 如果在排除列表中
String uri = request.getRequestURI();
Iterator<String> iterator = excludeList.iterator();
while (iterator.hasNext()) {
String exURI = iterator.next();
if(null != exURI && uri.contains(exURI)){
return true;
}
}
// 可以進行登錄和權(quán)限之類的判斷
LoginUser user = ControllerBase.getLoginUser(request);
if(null != user){
return true;
}
// 未登錄,不允許
return false;
}
private void initRequestThreadLocal(HttpServletRequest request){
ControllerBase.setRequest(request);
request.setAttribute("basePath", ControllerBase.basePathLessSlash(request));
}
private void removeRequestThreadLocal(){
ControllerBase.setRequest(null);
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
initRequestThreadLocal(request);
// 如果不允許操作,則返回false即可
if (false == validURI(request)) {
// 此處拋出異常,允許進行異常統(tǒng)一處理
throw new NeedLoginException();
}
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
removeRequestThreadLocal();
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
removeRequestThreadLocal();
}
}
在 postHandle 和 afterCompletion 方法中, 清理 ThreadLocal 中的 request 對象。
但在實際使用過程中, 業(yè)務(wù)開發(fā)人員將一個很大的對象(如占用內(nèi)存200MB左右的List)設(shè)置為 request 的 Attributes, 傳遞到 JSP 中。
JSP代碼中可能發(fā)生了異常, 則SpringMVC的postHandle 和 afterCompletion 方法不會被執(zhí)行。
Tomcat 中的線程調(diào)度, 可能會一直調(diào)度不到那個拋出了異常的線程, 于是 ThreadLocal 一直 hold 住 request。 隨著運行時間的推移,把可用內(nèi)存占滿, 一直在執(zhí)行 Full GC, 系統(tǒng)直接卡死。
后續(xù)的修正: 通過 Filter, 在 finally 語句塊中清理 ThreadLocal。
@WebFilter(value="/*", asyncSupported=true)
public class ClearRequestCacheFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
clearControllerBaseThreadLocal();
try {
chain.doFilter(request, response);
} finally {
clearControllerBaseThreadLocal();
}
}
private void clearControllerBaseThreadLocal() {
ControllerBase.setRequest(null);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
教訓(xùn)是:可以使用 ThreadLocal, 但必須有受控制的釋放措施、一般就是 try-finally 的代碼形式。
說明: SpringMVC 的 Controller 中, 其實可以通過 @Autowired 注入 request, 實際注入的是一個 HttpServletRequestWrapper 對象, 執(zhí)行時也是通過 ThreadLocal 機制調(diào)用當(dāng)前的 request。
常規(guī)方式: 直接在controller方法中接收 request 參數(shù)即可。
解決方案
如果設(shè)置的最大內(nèi)存不滿足程序的正常運行, 只需要增大堆內(nèi)存即可, 配置參數(shù)可以參考下文。
但很多情況下, 增加堆內(nèi)存空間并不能解決問題。比如存在內(nèi)存泄漏, 增加堆內(nèi)存只會推遲 java.lang.OutOfMemoryError: Java heap space 錯誤的觸發(fā)時間。
當(dāng)然, 增大堆內(nèi)存, 可能會增加 GC pauses 的時間, 從而影響程序的 吞吐量或延遲。
要從根本上解決問題, 則需要排查分配內(nèi)存的代碼. 簡單來說, 需要解決這些問題:
- 哪類對象占用了最多內(nèi)存?
- 這些對象是在哪部分代碼中分配的。
要搞清這一點, 可能需要好幾天時間。下面是大致的流程:
- 獲得在生產(chǎn)服務(wù)器上執(zhí)行堆轉(zhuǎn)儲(heap dump)的權(quán)限。“轉(zhuǎn)儲”(Dump)是堆內(nèi)存的快照, 稍后可以用于內(nèi)存分析. 這些快照中可能含有機密信息, 例如密碼、信用卡賬號等, 所以有時候, 由于企業(yè)的安全限制, 要獲得生產(chǎn)環(huán)境的堆轉(zhuǎn)儲并不容易。
- 在適當(dāng)?shù)臅r間執(zhí)行堆轉(zhuǎn)儲。一般來說,內(nèi)存分析需要比對多個堆轉(zhuǎn)儲文件, 假如獲取的時機不對, 那就可能是一個“廢”的快照. 另外, 每次執(zhí)行堆轉(zhuǎn)儲, 都會對JVM進行“凍結(jié)”, 所以生產(chǎn)環(huán)境中,也不能執(zhí)行太多的Dump操作,否則系統(tǒng)緩慢或者卡死,你的麻煩就大了。
- 用另一臺機器來加載Dump文件。一般來說, 如果出問題的JVM內(nèi)存是8GB, 那么分析 Heap Dump 的機器內(nèi)存需要大于 8GB. 打開轉(zhuǎn)儲分析軟件(我們推薦Eclipse MAT , 當(dāng)然你也可以使用其他工具)。
- 檢測快照中占用內(nèi)存最大的 GC roots。詳情請參考: Solving OutOfMemoryError (part 6) – Dump is not a waste。 這對新手來說可能有點困難, 但這也會加深你對堆內(nèi)存結(jié)構(gòu)以及navigation機制的理解。
- 接下來, 找出可能會分配大量對象的代碼. 如果對整個系統(tǒng)非常熟悉, 可能很快就能定位了。
打個廣告, 我們推薦 Plumbr, the only Java monitoring solution with automatic root cause detection。 Plumbr 能捕獲所有的 java.lang.OutOfMemoryError , 并找出其他的性能問題, 例如最消耗內(nèi)存的數(shù)據(jù)結(jié)構(gòu)等等。
Plumbr 在后臺負責(zé)收集數(shù)據(jù) —— 包括堆內(nèi)存使用情況(只統(tǒng)計對象分布圖, 不涉及實際數(shù)據(jù)),以及在堆轉(zhuǎn)儲中不容易發(fā)現(xiàn)的各種問題。 如果發(fā)生 java.lang.OutOfMemoryError , 還能在不停機的情況下, 做必要的數(shù)據(jù)處理. 下面是Plumbr 對一個 java.lang.OutOfMemoryError 的提醒:
強大吧, 不需要其他工具和分析, 就能直接看到:
- 哪類對象占用了最多的內(nèi)存(此處是 271 個 com.example.map.impl.PartitionContainer 實例, 消耗了 173MB 內(nèi)存, 而堆內(nèi)存只有 248MB)
- 這些對象在何處創(chuàng)建(大部分是在 MetricManagerImpl 類中,第304行處)
- 當(dāng)前是誰在引用這些對象(從 GC root 開始的完整引用鏈)
得知這些信息, 就可以定位到問題的根源, 例如是當(dāng)?shù)鼐啍?shù)據(jù)結(jié)構(gòu)/模型, 只占用必要的內(nèi)存即可。
當(dāng)然, 根據(jù)內(nèi)存分析的結(jié)果, 以及Plumbr生成的報告, 如果發(fā)現(xiàn)對象占用的內(nèi)存很合理, 也不需要修改源代碼的話, 那就增大堆內(nèi)存吧。在這種情況下,修改JVM啟動參數(shù), (按比例)增加下面的值:
-Xmx1024m
這里配置Java堆內(nèi)存最大為 1024MB。可以使用 g/G 表示 GB, m/M 代表 MB, k/K 表示 KB.
下面的這些形式都是等價的, 設(shè)置Java堆的最大空間為 1GB:
# 等價形式: 最大1GB內(nèi)存
java -Xmx1073741824 com.mycompany.MyClass
java -Xmx1048576k com.mycompany.MyClass
java -Xmx1024m com.mycompany.MyClass
java -Xmx1g com.mycompany.MyClass