美文网首页深入理解JVMJava
StackOverflowError 栈溢出实战

StackOverflowError 栈溢出实战

作者: xiaolyuh | 来源:发表于2019-11-26 13:16 被阅读0次

    栈溢出的原因

    在解决栈溢出问题之前,我们首先需要知道一般引起栈溢出的原因,主要有以下几点:

    1. 是否有递归调用
    2. 循环依赖调用
    3. 方法调用链路很深,层级到达10W左右就会出现栈溢出

    问题现象

    我们一个很老的接口(近一年没有动过)在线上运行一段时间后报了StackOverflowError栈溢出,其他接口又能正常提供服务,错误日志:

    java.lang.StackOverflowError
        org.springframework.web.servlet.DispatcherServlet.triggerAfterCompletionWithError(DispatcherServlet.java:1303)
        org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:977)
        org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
        org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
        org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
        javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
        org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
        javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
        org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
        com.wlqq.etc.deposit.web.filter.WebContextFilterDev.doFilter(WebContextFilterDev.java:78)
        org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
        org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
        com.wlqq.library.httpcommons.sso.filter.SSOSessionFilter.doFilter(SSOSessionFilter.java:95)
        org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
        org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
        org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
        org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        io.opentracing.contrib.web.servlet.filter.TracingFilter.doFilter(TracingFilter.java:187)
    root cause java.lang.StackOverflowError
        java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
        java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
        java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
        java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
        java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
        java.util.Collections$UnmodifiableCollection.get(Collections.java:1030)
    ...
    

    解决过程

    review代码

    首先按照上面栈溢出的原因,我对该接口的业务代码review一遍,但是很遗憾,没有发现有任何的问题,没有递归,循环依赖等。

    从日志上看错误日志是在DispatcherServlet中出现的,大致看了下DispatcherServlet也没看出啥问题,因为如果是框架的问题,那么也不应该就出现在这一个接口上。于是针对这个接口我也看了下他的查询语句,对应Mybatis的resultMap等。都没啥问题,就是一个简单的查询语句,resultMap也没有嵌套,返回实体也没有嵌套类,在正常不过了。

    本地重现

    因为这是线上环境,我们排查问题十分受限,而且是栈溢出,自己也确实不知道用啥命令和工具可以借助,于是我在本地将代码跑起来,用JMeter工具对该接口进行压测,果然,本地也出现了相同问题,能在本地重现我就松了一口气了,因为真相离我们已经很近了。

    使用断点

    我在DispatcherServlet报错位置打上了断点,结果debug栈出来后,我还是一无所获,因为栈信息就和上线爆出的信息一模一样。这个信息连问题具体是在DispatcherServlet代码中哪一行报出的都没法定位到。

    然后我将断点移动到Collections的1309行。通过不断的尝试我看到了这个debug栈:

    ...
    get:1309, Collections$UnmodifiableList (java.util) [7]
    get:1309, Collections$UnmodifiableList (java.util) [6]
    get:1309, Collections$UnmodifiableList (java.util) [5]
    get:1309, Collections$UnmodifiableList (java.util) [4]
    get:1309, Collections$UnmodifiableList (java.util) [3]
    get:1309, Collections$UnmodifiableList (java.util) [2]
    get:1309, Collections$UnmodifiableList (java.util) [1]
    handleResultSets:159, DefaultResultSetHandler (org.apache.ibatis.executor.resultset)
    query:63, PreparedStatementHandler (org.apache.ibatis.executor.statement)
    query:78, RoutingStatementHandler (org.apache.ibatis.executor.statement)
    doQuery:62, SimpleExecutor (org.apache.ibatis.executor)
    queryFromDatabase:303, BaseExecutor (org.apache.ibatis.executor)
    query:154, BaseExecutor (org.apache.ibatis.executor)
    query:102, CachingExecutor (org.apache.ibatis.executor)
    query:82, CachingExecutor (org.apache.ibatis.executor)
    invoke:-1, GeneratedMethodAccessor98 (sun.reflect)
    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
    invoke:498, Method (java.lang.reflect)
    proceed:49, Invocation (org.apache.ibatis.plugin)
    intercept:85, PageInterceptor (com.wlqq.etc.deposit.common.interceptor)
    invoke:61, Plugin (org.apache.ibatis.plugin)
    query:-1, $Proxy66 (com.sun.proxy)
    selectList:120, DefaultSqlSession (org.apache.ibatis.session.defaults)
    selectList:113, DefaultSqlSession (org.apache.ibatis.session.defaults)
    invoke:-1, GeneratedMethodAccessor100 (sun.reflect)
    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
    invoke:498, Method (java.lang.reflect)
    invoke:386, SqlSessionTemplate$SqlSessionInterceptor (org.mybatis.spring)
    selectList:-1, $Proxy49 (com.sun.proxy)
    selectList:205, SqlSessionTemplate (org.mybatis.spring)
    executeForMany:122, MapperMethod (org.apache.ibatis.binding)
    execute:64, MapperMethod (org.apache.ibatis.binding)
    invoke:53, MapperProxy (org.apache.ibatis.binding)
    queryOpenCardOrders:-1, $Proxy132 (com.sun.proxy)
    queryOpenCardOrders:1506, OpenCardServiceImpl (com.wlqq.etc.deposit.service.impl)
    invoke:-1, GeneratedMethodAccessor113 (sun.reflect)
    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
    invoke:498, Method (java.lang.reflect)
    invokeJoinpointUsingReflection:302, AopUtils (org.springframework.aop.support)
    invoke:201, JdkDynamicAopProxy (org.springframework.aop.framework)
    queryOpenCardOrders:-1, $Proxy154 (com.sun.proxy)
    queryOpenCardOrders:90, OpenCardOrderController (com.wlqq.etc.deposit.web.controller)
    invoke:-1, OpenCardOrderController$$FastClassBySpringCGLIB$$1a780c6e (com.wlqq.etc.deposit.web.controller)
    invoke:204, MethodProxy (org.springframework.cglib.proxy)
    invokeJoinpoint:717, CglibAopProxy$CglibMethodInvocation (org.springframework.aop.framework)
    proceed:157, ReflectiveMethodInvocation (org.springframework.aop.framework)
    proceed:85, MethodInvocationProceedingJoinPoint (org.springframework.aop.aspectj)
    doAround:62, RequestLogAOP (com.wlqq.etc.deposit.web.filter)
    invoke:-1, GeneratedMethodAccessor112 (sun.reflect)
    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
    invoke:498, Method (java.lang.reflect)
    invokeAdviceMethodWithGivenArgs:621, AbstractAspectJAdvice (org.springframework.aop.aspectj)
    invokeAdviceMethod:610, AbstractAspectJAdvice (org.springframework.aop.aspectj)
    invoke:68, AspectJAroundAdvice (org.springframework.aop.aspectj)
    proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework)
    proceed:85, MethodInvocationProceedingJoinPoint (org.springframework.aop.aspectj)
    doAround:67, ValidateArgsAOP (com.wlqq.library.validate.aop)
    invoke:-1, GeneratedMethodAccessor111 (sun.reflect)
    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
    invoke:498, Method (java.lang.reflect)
    invokeAdviceMethodWithGivenArgs:621, AbstractAspectJAdvice (org.springframework.aop.aspectj)
    invokeAdviceMethod:610, AbstractAspectJAdvice (org.springframework.aop.aspectj)
    invoke:68, AspectJAroundAdvice (org.springframework.aop.aspectj)
    proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework)
    invoke:92, ExposeInvocationInterceptor (org.springframework.aop.interceptor)
    proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework)
    intercept:653, CglibAopProxy$DynamicAdvisedInterceptor (org.springframework.aop.framework)
    queryOpenCardOrders:-1, OpenCardOrderController$$EnhancerBySpringCGLIB$$6931dba9 (com.wlqq.etc.deposit.web.controller)
    invoke:-1, GeneratedMethodAccessor110 (sun.reflect)
    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
    invoke:498, Method (java.lang.reflect)
    doInvoke:221, InvocableHandlerMethod (org.springframework.web.method.support)
    invokeForRequest:137, InvocableHandlerMethod (org.springframework.web.method.support)
    invokeAndHandle:110, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
    invokeHandlerMethod:806, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
    handleInternal:729, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
    handle:85, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
    doDispatch:959, DispatcherServlet (org.springframework.web.servlet)
    doService:893, DispatcherServlet (org.springframework.web.servlet)
    processRequest:970, FrameworkServlet (org.springframework.web.servlet)
    doPost:872, FrameworkServlet (org.springframework.web.servlet)
    service:648, HttpServlet (javax.servlet.http)
    service:846, FrameworkServlet (org.springframework.web.servlet)
    service:729, HttpServlet (javax.servlet.http)
    internalDoFilter:292, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
    internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:78, WebContextFilterDev (com.wlqq.etc.deposit.web.filter)
    invokeDelegate:346, DelegatingFilterProxy (org.springframework.web.filter)
    doFilter:262, DelegatingFilterProxy (org.springframework.web.filter)
    internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:95, SSOSessionFilter (com.wlqq.library.httpcommons.sso.filter)
    invokeDelegate:346, DelegatingFilterProxy (org.springframework.web.filter)
    doFilter:262, DelegatingFilterProxy (org.springframework.web.filter)
    internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
    doFilterInternal:85, CharacterEncodingFilter (org.springframework.web.filter)
    doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
    internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:187, TracingFilter (io.opentracing.contrib.web.servlet.filter)
    internalDoFilter:240, ApplicationFilterChain (org.apache.catalina.core)
    doFilter:207, ApplicationFilterChain (org.apache.catalina.core)
    invoke:212, StandardWrapperValve (org.apache.catalina.core)
    invoke:106, StandardContextValve (org.apache.catalina.core)
    invoke:502, AuthenticatorBase (org.apache.catalina.authenticator)
    invoke:141, StandardHostValve (org.apache.catalina.core)
    invoke:79, ErrorReportValve (org.apache.catalina.valves)
    invoke:616, AbstractAccessLogValve (org.apache.catalina.valves)
    invoke:88, StandardEngineValve (org.apache.catalina.core)
    service:528, CoyoteAdapter (org.apache.catalina.connector)
    process:1099, AbstractHttp11Processor (org.apache.coyote.http11)
    process:670, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
    doRun:2508, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
    run:2497, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
    runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
    run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
    run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
    run:745, Thread (java.lang)
    

    看到这个deug栈和我们的报错就很类似,于是我又看了一下List对象,直接提示了栈溢出:

    image.png

    通过上图我确认我找对了位置,然后根据debug栈,找这个list的源头。

    我发现这个list就是mybatis的resultMaps,在DefaultResultSetHandler#handleResultSets方法中的resultMaps也报了栈溢出,resultMaps又来自mappedStatement,于是我们只要找到mappedStatement源头就行了。

    DefaultSqlSession#selectListObject, RowBounds)方法中我找到了MappedStatement的源头,它是直接从Mybatis的configuration对象中获取的一个缓存对象。

      @Override
      public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
          MappedStatement ms = configuration.getMappedStatement(statement);
          return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    通过断点信息我发现ms对象的resultMaps属性是正常的。而且我惊奇的发现这个ms对象和我们后面报错的mappedStatement对象不是同一个对象,于是我猜测后面又代码将这个mappedStatement给改了。然后我通过查看debug栈我发现,在分页插件中,为了实现分页它会将mappedStatement对象给改了。

    问题根源

    从上面我们定位到了是分页插件中getPageStatement()方法,将Mybatis的mappedStatement给改了,下面是源码我们看下是如何修改的:

    @Intercepts({@Signature(
            type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
    public class PageInterceptor implements Interceptor {
        private static final int MAPPED_STATEMENT_INDEX = 0;
        private static final int PARAMETER_INDEX = 1;
        private static final int ROWBOUNDS_INDEX = 2;
        private static final String sql = "sql", SQLSOURCE_STRING = "sqlSource";
        private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
        private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
        private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
        private static final Map<String, Builder> BUILDER_MAP = new HashMap<String, Builder>();
        //处理SQL
        public static final SqlParser sqlParser = new SqlParser();
    
        @SuppressWarnings({"unchecked", "rawtypes"})
        @Override
        public Object intercept(final Invocation invocation) throws Throwable {
            final Object[] queryArgs = invocation.getArgs();
            final MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
            final BoundSql boundSql = ms.getBoundSql(queryArgs[PARAMETER_INDEX]);
            final Object paramObj = boundSql.getParameterObject();
            Page<?> page = null;
            if (paramObj instanceof MapperMethod.ParamMap) {    //如果为多参数
                for (Object value : ((MapperMethod.ParamMap) paramObj).values()) {
                    if (value instanceof Page) {
                        page = (Page<?>) value;
                        break;
                    }
                }
            }
    
            if (paramObj instanceof Page) {    //如果参数为单个page对象
                page = (Page<?>) paramObj;
            }
    
            if (page != null) {
                int count = getCount(((Executor) invocation.getTarget()).getTransaction().getConnection(), boundSql, paramObj, ms);
                page.setTc(count);
                if (count != 0) {
                    queryArgs[ROWBOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
                    queryArgs[MAPPED_STATEMENT_INDEX] = getPageStatement(ms, boundSql, page);
                    page.setDatas((List) invocation.proceed());
                }
                return page.getDatas();
            }
            return invocation.proceed();
        }
    
        private static final MappedStatement getPageStatement(MappedStatement ms, BoundSql boundSql, Page<?> page) {
            String id = ms.getId();
            Builder builder = BUILDER_MAP.get(id);
            if (builder == null) {
                builder = new Builder(ms.getConfiguration(), ms.getId(), new ExtSqlSource(boundSql), ms.getSqlCommandType());
                builder.resource(ms.getResource());
                builder.fetchSize(ms.getFetchSize());
                builder.statementType(ms.getStatementType());
                builder.keyGenerator(ms.getKeyGenerator());
                if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
                    StringBuffer keyProperties = new StringBuffer();
                    for (String keyProperty : ms.getKeyProperties()) {
                        keyProperties.append(keyProperty).append(",");
                    }
                    keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
                    builder.keyProperty(keyProperties.toString());
                }
    
                builder.timeout(ms.getTimeout());
    
                builder.parameterMap(ms.getParameterMap());
    
                builder.resultMaps(ms.getResultMaps());
                builder.resultSetType(ms.getResultSetType());
    
                builder.cache(ms.getCache());
                builder.flushCacheRequired(ms.isFlushCacheRequired());
                builder.useCache(ms.isUseCache());
                BUILDER_MAP.put(id, builder);
            }
    
            ms = builder.build();
    
            MetaObject.forObject(boundSql, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY)
                    .setValue(sql, getPageSql(boundSql.getSql(), page));
    
            MetaObject.forObject(ms, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY)
                    .setValue(SQLSOURCE_STRING, new ExtSqlSource(boundSql));
            return ms;
        }
    
        private static final int getCount(Connection connection, BoundSql boundSql, Object paramObj,
                                          MappedStatement mappedStatement) {
            int count = 0;
            ResultSet rs = null;
            PreparedStatement countStmt = null;
            try {
                final String countSql = getCountSql(boundSql.getSql());
                countStmt = connection.prepareStatement(countSql);
                final DefaultParameterHandler handler = new DefaultParameterHandler(mappedStatement, paramObj, boundSql);
                handler.setParameters(countStmt);
                rs = countStmt.executeQuery();
                if (rs.next()) {
                    count = rs.getInt(1);
                }
    
            } catch (SQLException e) {
                throw new SystemException("SQL invalid", e);
            } finally {
                try {
                    if (rs != null) {
                        rs.close();
                    }
                    if (countStmt != null) {
                        countStmt.close();
                    }
                } catch (SQLException e) {
                    //throw new SystemException("SQL invalid", e);
                    e.printStackTrace();
                }
    
            }
            return count;
        }
    
        private static String getCountSql(String originalSql) {    //count sql
            return sqlParser.getSmartCountSql(originalSql);
        }
    
    
        private static String getPageSql(String originalSql, Page<?> page) {
            return originalSql + " limit " + page.getStart() + "," + page.getPs();
        }
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties props) {
        }
    
    
        private static class ExtSqlSource implements SqlSource {
            BoundSql boundSql;
    
            protected ExtSqlSource(BoundSql boundSql) {
                this.boundSql = boundSql;
            }
    
            @Override
            public BoundSql getBoundSql(Object parameterObject) {
                return boundSql;
            }
        }
    }
    

    我们可以看到这个Mybatis分页插件的实现原理是,通过每次修改MappedStatement对象中的SQL语句来实现的分页。这段代码缓存了MappedStatement.Builder对象,通MappedStatement.Builder#build()对象来构建MappedStatement对象。在这里就出现了第一个错误点,它直接使用的是HashMap来缓存对象,HashMap是线程不安全的,如果是jdk1.7以前,HashMap在扩容的时候会发生循环调用,进而导致栈溢出,这里应该使用ConcurrentHashMap来做缓存。但是我们的问题不是HashMap引起的,因为我们用的是JDK1.8,并且在我压测过程中并没有发生扩容。

    于是我有看了一下MappedStatement.Builder#build()方法源码,代码如下:

    public MappedStatement build() {
        assert mappedStatement.configuration != null;
        assert mappedStatement.id != null;
        assert mappedStatement.sqlSource != null;
        assert mappedStatement.lang != null;
        mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
        return mappedStatement;
    }
    

    通过这段代码我发现,前面全是判断,最后就是对resultMaps做了一下装饰,Collections.unmodifiableList的主要作用就是将我们的list变成一个不可以修改的list,源码如下:

    public static <T> List<T> unmodifiableList(List<? extends T> list) {
        return (list instanceof RandomAccess ?
                new Collections.UnmodifiableRandomAccessList<>(list) :
                new Collections.UnmodifiableList<>(list));
    }
    

    看到这段代码我刚以为找到了根源,但是看下源码,就失望了,这段代码太正常不过,就是对原来的list装饰了一下,然后将一些修改方法给屏蔽了。

    于是我又倒回去看了下MappedStatement.Builder源码,发现了一个关键点,我们的MappedStatement的构建是使用的建造者模式,每个Builder````对象会去建造一个MappedStatement```,源码如下:

    public static class Builder {
        private MappedStatement mappedStatement = new MappedStatement();
    
        public MappedStatement build() {
            assert mappedStatement.configuration != null;
            assert mappedStatement.id != null;
            assert mappedStatement.sqlSource != null;
            assert mappedStatement.lang != null;
            mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
            return mappedStatement;
        }
    }
    

    通过这段代码我发现,每次调用MappedStatement.Builder#build()方法返回的同一个mappedStatement对象,并不是我们我们想的那样,每次build()方法会返回不同的对象。这就引出的这个插件的第二个错误点,在PageInterceptor#getPageStatement()方法中有如下代码:

    private static final MappedStatement getPageStatement(MappedStatement ms, BoundSql boundSql, Page<?> page) {
            String id = ms.getId();
            Builder builder = BUILDER_MAP.get(id);
            if (builder == null) {... }
    
            ms = builder.build();
    
            MetaObject.forObject(boundSql, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY)
                    .setValue(sql, getPageSql(boundSql.getSql(), page));
    
            MetaObject.forObject(ms, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY)
                    .setValue(SQLSOURCE_STRING, new ExtSqlSource(boundSql));
            return ms;
        }
    

    ms每次是同一个对象,在后续我们为了实现分页将该对象的sql给改了,在并发情况下,因为同时修改了同一个共享变量,会导致后续分页会时出现数据错乱的现象。但是这个错误和我们这次需要定位的问题没太大关系。

    但是通过分析我确定问题一定是出现在了这行代码身上

    ms = builder.build();
    

    于是我又倒回去看了build()源码:

    public MappedStatement build() {
        ...
        mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
        return mappedStatement;
    }
    

    有效代码就只有一行,通过上面分析我们发现,每次build的时候mappedStatement是同一个对象,那么每次build()的时候,这段代码就会将自己给装饰一次,源码如下:

    mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
    

    如果请求量大,这行代码就是在对自己不停的装饰,效果如下:

    Collections.unmodifiableList(
            Collections.unmodifiableList(
                    Collections.unmodifiableList(
                            Collections.unmodifiableList(
                                    Collections.unmodifiableList(
                                            Collections.unmodifiableList(
                                                    Collections.unmodifiableList(
                                                            ...)))))));
    

    当层级达到一定数量后,我们再调用这个listget方法时就会发生调用链太长,进而将方法栈撑爆,出现栈溢出。到这里就找到了问题的根源。

    解决方案

    1. 问题根源就是我们缓存了MappedStatement.Builder对象,我们去掉缓存后,代码恢复了正常。
    2. 我们不去新创建MappedStatement,直接修改原有MappedStatement的sql语句,在原来sql语句后面加上limit ?,?,最后分页信息通过参数传入。

    倒推问题答案

    1. 为什么这个问题以前运行得好好的,直到几年后的今天才被发现?
      这是因为我们以前这个服务发版很频繁,导致每次发版后这个装饰的层级被清空了。
    2. 为什么只有这一个接口出现了问题?
      这是因为这个接口是使用分页查询接口中访问量最大的那个接口,所以它最先出现问题。
    3. 为什么线上只有那么一两台机器出现问题?
      这是因为出问题的机器负载高一些,到时这些机器先出现问题。

    总结

    1. 栈溢出的原因基本上就是我上面列举的那些,但是我们在编写程序的过程中都会有意识的避开这些问题,所以线上出现栈溢出的可能性很小,但是一旦出现就不好排查。我们需要静下心来慢慢分析,总会找到问题根源的,只是过程有点痛苦。
    2. 在没有完全了解Mybatis运行原理的情况下,不建议做Mybatis的插件开发。
    3. 没动过代码不代表系统就不会出现问题。

    相关文章

      网友评论

        本文标题:StackOverflowError 栈溢出实战

        本文链接:https://www.haomeiwen.com/subject/bqvjwctx.html