利用session作国际化引发的old区内存爆满及修复方法

题记:昨天加班打车回家,看见前面有辆路虎在高速上开的巨慢,挡住了我坐的出租车的路,因而就跟司机吐槽了一句:“前面这车怎么这么面啊?”,司机沉默了大概3秒,说了一句富含哲理性的话:“没有面车,只有面人”。借用这句话套在软件开发上就是:“没有面代码,只有面的程序猿”。只不过此次我就是那个面的程序猿。(面:是一个方言,大意就是优柔寡断,反应迟缓,没有主见,好欺负之类的)。html

背景是这样的,最近项目要作国际化,主要是基于spring的i18来作,经过拦截器拦截request过来的url中是否包含locale参数,若是locale参数指定了语言类型,则页面上显示的信息按指定的类型来获取,如content这个字段,若是locale=zh_CN,则content=“你好”,若是locale=en_US,则content=“hello”;返回页面的时候,一样根据locale,在返回页面的路径前加上具体目录,如返回sayHi.html,locale=zh_CN时返回cn/sayHi.html,locale=en_US时返回us/sayHi.html,这样能够显示不一样内容的欢迎页面,通常国际化也是这样作的,同一套代码,不一样配置就能够完成国际化。web

因而开始的时候个人拦截器代码时这样写的:spring

public class LocaleHandleInterceptor extends LocaleChangeInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {
        String newLocale = request.getParameter(this.getParamName());
        if (newLocale != null) {
            LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
            if (localeResolver == null) {
                throw new IllegalStateException("No LocaleResolver found,may be not config localeResolver bean!");
            }
            Locale locale = StringUtils.parseLocaleString(newLocale);
            localeResolver.setLocale(request, response, locale);
            request.getSession().setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale);
        } else {
            Object localObject = request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
            if (null == localObject) {
                Locale locale = RequestContextUtils.getLocale(request);
                locale = (null == locale) ? Locale.getDefault() : locale;
                request.getSession().setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale);
            }
        }
        // Proceed in any case.
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (null != modelAndView) {
            // Locale locale =
            // (Locale)request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
            Locale locale = (Locale) WebUtils.getSessionAttribute(request, SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
            modelAndView.setViewName(locale.toString() + "/" + modelAndView.getViewName());
        }
    }

}

 

写完后跑了几个例子,换了一个locale参数设置都没有问题,这件事情就算完成了。json

过了一段时间后进行上线前的压力测试,用loadrunner 500用户并发压测tomcat(jvm Xmx=1024M)的时候发现曲线比较奇怪,刚开始表现正常,但大约30秒后tps急剧降低,一分钟后tps趋于0,可是tomcat并无报OOM内存溢出,用浏览器访问某个url仍是能有返回,用gcutil查看内存回收状况,发现每秒都在作full gc,因而用jmap把当前tomcat的堆栈打出来,看到不少concurrentHashMap对象,对象里有不少locale对象,联想到这块代码最近只加过国际化的拦截器,因而怀疑到是否是session致使的,一开始没有改代码,而是在tomcat的conf下找到web.xml,调整了session-timeout 为1分钟,从新压测问题仍是复现,从新审视代码,发现是这段代码引发的问题,把它注释掉就正常:浏览器

// Object localObject = request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);

 

找到问题后,对request.getSession()进行分析,此方法内部调用的是request.getSession(true),即没有session的时候会建立,压力测试下大并发访问致使过多的session对象建立用于存储locale,在必定mina gc后都移到jvm old区,引发频繁的full gc,拖慢整个响应。那为何没有报OOM呢?我猜想是响应慢后拖慢了压测机打过来的请求,full gc每次都能清理出一些空间出来,避免了OOM。不知道是否是这样?基于这种session的方式自己是能够作国际化的,只是我调用的方式不对,因而参考spring的LocaleChangeInterceptor和SessionLocaleResolver类的源码将咱们的拦截器代码修改以下:tomcat

public class LocaleHandleInterceptor extends LocaleChangeInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws ServletException {
        String localName = request.getParameter(this.getParamName());
        LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
        if (localeResolver == null) {
            throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
        }
        Locale locale = localeResolver.resolveLocale(request);
        if(!(null ==localName || locale.toString().equalsIgnoreCase(localName))){
            localeResolver.setLocale(request, response, (null == localName?Locale.getDefault():StringUtils.parseLocaleString(localName)));
        }
        // Proceed in any case.
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //Locale locale = (Locale)request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
        if(null != modelAndView){
            // 过滤json返回的状况
            Locale locale = RequestContextUtils.getLocaleResolver(request).resolveLocale(request);
            modelAndView.setViewName(locale.toString()+"/"+modelAndView.getViewName());
        }
    }

}

以后再压测就没问题了。session

后记:不通过压测的代码不足以谈优秀,出了任何问题,先抛开以前的先入为主根深蒂固的观念,从源头分析,大胆猜想,当心求证,并保持学习的心态,绕开问题很容易,可是直面问题须要勇气和耐心,踏过了它,你就会前进一步,而技术上的前进是技术人员真正的财富。并发

相关文章
相关标签/搜索