最近發現線上的一個應用跑一段週期之後就會出現內存溢出問題,需要重啓,所以以此分析一下原因並修復。
1.獲取內存快照
使用jdk自帶工具jmap獲取內存快照文件,如下:
jmap -dump:format=b,file=快照保存路徑 進程id
如:
jmap -dump:format=b,file=/opt/27245.hprof 27245
2.使用工具進行分析
獲取到內存快照文件27245.hprof後,我們需要對此進行分析,這裏使用的分析工具爲Eclipse Memory Analyzer Tool
,我們使用工具打開27245.hprof文件。
2.1 Main View
主界面顯示有一大塊內存佔用高達1.3G,基本可以確定是這一塊出現了問題。
J2EE Spring redirect導致內存溢出問題_圖例1
2.2 Dominator Tree
繼續點擊主界面下面的Action
-> Dominator Tree
Dominator Tree(支配樹)是一個對象圖, 它將對象的引用關係轉換成一種樹形的對象圖結構. 通過它可以很輕鬆地看出對象的引用關係以及哪些最大的內存使用塊.
點擊打開之後,如下圖:
J2EE Spring redirect導致內存溢出問題_圖例2
我們可以看到第一行這個對象佔用了最多的資源,高達97%,我們繼續點擊第一行的AnnotationAwareAspectJAutoProxyCreator
,看到如下圖:
J2EE Spring redirect導致內存溢出問題_圖例3
可以看到這裏有個Map存儲了大量的String值,觀察內容,發現String都帶有redirect:http
字樣,從這就可以看出,我們的應用應該是在重定向這一塊出現了問題。
3.應用代碼分析
在應用內全局搜索redirect:http
字符串,可以發現應用裏面使用了類似以下方法進行重定向。
return "redirect:" + sb.toString();
3.1.Spring 3.x版本通過搜索引擎檢索發現,這是spring的一個bug,具體在3.x,4.x,5.x版本都未進行修復。
AbstractCachingViewResolver裏面的viewCache HashMap沒有限制大小。導致瞭如果在 controller 返回的 view 是不固定的,如:”redirect:form.html?entityId=” + entityId,由於 entityId 的值會存在 N 個,那麼會導致產生 N 個 ViewName 被緩存起來,此處並沒有進行處理,所以如果項目上馬使用了類似動態拼接參數的重定向操作,應用程序運行久了就會出現內存佔用過高的問題。
3.2.Spring 4.x版本
4.x 版本及以上已經修復上述問題,代碼如下:
但是4.x版本修復了以上問題還存在着另一個問題:
public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {
/**
* Default maximum number of entries for the view cache: 1024
*/
public static final int DEFAULT_CACHE_LIMIT = 1024;
/**
* Dummy marker object for unresolved views in the cache Maps
*/
private static final View UNRESOLVED_VIEW = new View() {
@Override
public String getContentType() {
return null;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
}
};
/**
* The maximum number of entries in the cache
*/
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
/**
* Whether we should refrain from resolving views again if unresolved once
*/
private boolean cacheUnresolved = true;
/**
* Fast access cache for Views, returning already cached instances without a global lock
*/
private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT);
/**
* Map from view key to View instance, synchronized for View creation
*/
@SuppressWarnings("serial")
private final Map<Object, View> viewCreationCache =
new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {
if (size() > getCacheLimit()) {
// 超過限制大小,刪除老數據。
viewAccessCache.remove(eldest.getKey());
return true;
} else {
return false;
}
}
};
}
AbstractAutoProxyCreator這個類裏面的advisedBeans ConcurrentHashMap沒有限制大小,所以同樣會出現內存溢出問題。
3.3.Spring 5.x版本
5.x版本也仍未修復此問題。
4.bug修復
Spring未對redirect:
這個動態拼接鏈接的bug進行修復,我們可以使用以下原生的servlet方式進行重定向,從而避免使用spring redirect產生的內存溢出問題,具體修改如下:
未修改的代碼:
@RequestMapping(value = "/getOpenid", method = RequestMethod.GET)
public String getOpenid(HttpServletRequest request, HttpServletResponse response) {
if (...){
...
return "redirect:" + sb.toString();
}else{
...
return "rePage";
}
}
修改後的代碼:
@RequestMapping(value = "/getOpenid", method = RequestMethod.GET)
public String getOpenid(HttpServletRequest request, HttpServletResponse response) {
if (...) {
...
response.sendRedirect(sb.toString());
return null;
}else{
...
return "rePage";
}
}
5.參考鏈接