当前位置:   article > 正文

springboot重定向导致的内存泄漏分析

springboot 解析hprof

之前在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文件进行分析
分析图如下

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI2MDkzMzQx,size_16,color_FFFFFF,t_70

 

 可以发现存在内存泄漏的情况,罪魁祸首是被ConcurrentHashMap(advisedBeans)引用的字符串,查看map的内容,发现如下,ref的key全部都是redirect开头的字符串,所以判断问题出在springmvc的redirect上

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI2MDkzMzQx,size_16,color_FFFFFF,t_70

但是仅仅知道问题出在这里,还是不知道怎么解决,所以下面是简单的源码的一个分析过程,只写关键部分,可以按步骤自己对着源码看

  首先将断点定位到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;
        }
    }
至此,分析结束,也验证了我们的猜想,谢谢您看到这里

转载于:https://my.oschina.net/u/3081965/blog/3050088

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/164568
推荐阅读
相关标签
  

闽ICP备14008679号