J2EE Spring redirect导致内存溢出问题

  |  
阅读次数
  |  
字数 1,041
  |  
时长 ≈ 5 分钟

最近发现线上的一个应用跑一段周期之后就会出现内存溢出问题,需要重启,所以以此分析一下原因并修复。

1.获取内存快照

使用jdk自带工具jmap获取内存快照文件,如下:

1
2
3
4
5
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字符串,可以发现应用里面使用了类似以下方法进行重定向。

1
return "redirect:" + sb.toString();

通过搜索引擎检索发现,这是spring的一个bug,具体在3.x,4.x,5.x版本都未进行修复。

3.1.Spring 3.x版本

AbstractCachingViewResolver里面的viewCache HashMap没有限制大小。导致了如果在 controller 返回的 view 是不固定的,如:”redirect:form.html?entityId=” + entityId,由于 entityId 的值会存在 N 个,那么会导致产生 N 个 ViewName 被缓存起来,此处并没有进行处理,所以如果项目上马使用了类似动态拼接参数的重定向操作,应用程序运行久了就会出现内存占用过高的问题。

3.2.Spring 4.x版本

4.x 版本及以上已经修复上述问题,代码如下:

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
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;
}
}
};
}

但是4.x版本修复了以上问题还存在着另一个问题

AbstractAutoProxyCreator这个类里面的advisedBeans ConcurrentHashMap没有限制大小,所以同样会出现内存溢出问题。

3.3.Spring 5.x版本

5.x版本也仍未修复此问题。

4.bug修复

Spring未对redirect:这个动态拼接链接的bug进行修复,我们可以使用以下原生的servlet方式进行重定向,从而避免使用spring redirect产生的内存溢出问题,具体修改如下:

未修改的代码:

1
2
3
4
5
6
7
8
9
10
@RequestMapping(value = "/getOpenid", method = RequestMethod.GET)
public String getOpenid(HttpServletRequest request, HttpServletResponse response) {
if (...) {
...
return "redirect:" + sb.toString();
}else{
...
return "rePage";
}
}

修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value = "/getOpenid", method = RequestMethod.GET)
public String getOpenid(HttpServletRequest request, HttpServletResponse response) {
if (...) {
...
response.sendRedirect(sb.toString());
return null;
}else{
...
return "rePage";
}
}

5.参考链接

Eclipse MAT

spring redirect 导致内存溢出问题核查