之前在csdn写过一次,今天晚上又遇到这个问题,在spring boot中还依旧存在。但是最近在找工作,所以copy到os中国,看看有没有面试机会,如果您有坑位,麻烦联系我哈!base 广深
========================================================================================
@RequestMapping(method = RequestMethod.GET)
public String test(String redirectUrl){
return "redirect:"+redirectUrl;
}
========================================================================================
项目内一个热点接口,使用了如上代码做代码的重定向操作,而让人没想到的是:这样子的写法会有内存泄漏的风险
如果不处理,那么随着内存会耗尽。最终会导致频繁的fullgc,而OOM:gc overhead limit exceeded
先说原因:因为redirectUrl是带参数,动态的链接,redirect会构建ViewResolver#create一个RedirectView,执行applyLifecycleMethods去initBean的时候,会使用了一个名为AdvisedBeans的ConcurrentHashMap去保存,这个map以redirect:redirectUrl为key,又没有做limit容量限制,而redirectUrl是一个动态的链接,所以每次redirect都会生成put一条数据到map中
图为pinpoint监控下,运行多天后,最近一天的内存变化图,最后内存降下来是因为修改后重启了。
温馨提示:如果您对分析过程没兴趣,那么看到这里就可以停止了,解决方案之一是采用HttpServletResponse的sendRedirect就可以解决。
以下为我今天的处理、分析过程
发现fullgc如此频繁之后,首先是使用jmap dump出对应的堆hprof文件, jmap -dump:format=b,file=map.hprof pid
使用jstack导出线程信息, jstack pid >> stack.log
保存好gc的日志,gc.log,因为我们在一开始的jvm启动参数上就会对gc日志做保存
重启项目,防止oom
下载mat工具对map.hprof文件进行分析
分析图如下
可以发现存在内存泄漏的情况,罪魁祸首是被ConcurrentHashMap(advisedBeans)引用的字符串,查看map的内容,发现如下,ref的key全部都是redirect开头的字符串,所以判断问题出在springmvc的redirect上
但是仅仅知道问题出在这里,还是不知道怎么解决,所以下面是简单的源码的一个分析过程,只写关键部分,可以按步骤自己对着源码看
首先将断点定位到org.springframework.web.servlet.DispatcherServlet#doService()=>doDispatch()下,至于为什么?可能您需要了解一下springmvc的请求处理流程
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
//....
this.doDispatch(request, response);
}
在doDispatch()的最后,会调用org.springframework.web.servlet.DispatcherServlet#processDispatchResult()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
//....
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
}
org.springframework.web.servlet.DispatcherServlet#render(),从而调用#resolverViewName()进行viewName的解析
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
//....
if (mv != null && !mv.wasCleared()) {
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
//....
}
org.springframework.web.servlet.view.AbstractCachingViewResolver#resolverViewName(),在这里会进行一次缓存的判断,生成一个cacheKey,cacheKey是由viewName(redirect:redirectUrl)+"_"+locale组成,存放到一个名为viewAccessCache的ConcurrentHashMap中,需要注意的是,这个map有做容量的上限限制为1024,具体做法是在存放到map的同时,也会存放一份到LinkedHashMap中,通过#removeEldestEntry去实现,所以如果redirectUrl是固定的,那么在第二次访问的时候,会直接命中缓存,也不会有内存泄漏的问题
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
//....
view = viewResolver.resolveViewName(viewName, locale);
//....
}
private final Map<Object, View> viewAccessCache = new ConcurrentHashMap(1024);
private final Map<Object, View> viewCreationCache = new LinkedHashMap<Object, View>(1024, 0.75F, true) {
protected boolean removeEldestEntry(Entry<Object, View> eldest) {
if (this.size() > AbstractCachingViewResolver.this.getCacheLimit()) {
AbstractCachingViewResolver.this.viewAccessCache.remove(eldest.getKey());
return true;
} else {
return false;
}
}
};
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (!this.isCache()) {
return this.createView(viewName, locale);
} else {
Object cacheKey = this.getCacheKey(viewName, locale);
View view = (View)this.viewAccessCache.get(cacheKey);
if (view == null) {
Map var5 = this.viewCreationCache;
synchronized(this.viewCreationCache) {
view = (View)this.viewCreationCache.get(cacheKey);
if (view == null) {
view = this.createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Cached view [" + cacheKey + "]");
}
}
}
}
}
return view != UNRESOLVED_VIEW ? view : null;
}
}
调用org.springframework.web.servlet.view.UrlBasedViewResolver#createView(),判断到viewName startWith redirect:的话,那么会构建一个RedirectView,并调用org.springframework.web.servlet.view.UrlBasedViewResolver的#applyLifecycleMethods()
protected View createView(String viewName, Locale locale) throws Exception {
//....
if (viewName.startsWith("redirect:")) {
forwardUrl = viewName.substring("redirect:".length());
RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
view.setHosts(this.getRedirectHosts());
return this.applyLifecycleMethods(viewName, view);
}
//....
}
private View applyLifecycleMethods(String viewName, AbstractView view) {
return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
}
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean()
public Object initializeBean(Object existingBean, String beanName) {
return this.initializeBean(beanName, existingBean, (RootBeanDefinition)null);
}
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization()
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
//....
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = this.applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
//....
}
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization()
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {
Object result = existingBean;
Iterator var4 = this.getBeanPostProcessors().iterator();
do {
if (!var4.hasNext()) {
return result;
}
BeanPostProcessor beanProcessor = (BeanPostProcessor)var4.next();
result = beanProcessor.postProcessAfterInitialization(result, beanName);
} while(result != null);
return result;
}
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary()
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean != null) {
Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
if (!this.earlyProxyReferences.contains(cacheKey)) {
return this.wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
在这里会将viewName作为key,value=Boolean.False存入名advisedBeans的concurrentHashMap中,需要注意的是,这个map的无限的。
private final Map<Object, Boolean> advisedBeans = new ConcurrentHashMap(256);
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
return bean;
} else if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
} else if (!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) {
Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
} else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
} else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
}
至此,分析结束,也验证了我们的猜想,谢谢您看到这里