StackOverflowError 栈溢出实战

栈溢出的原因

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

  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. 没动过代码不代表系统就不会出现问题。
发布了203 篇原创文章 · 获赞 145 · 访问量 85万+

猜你喜欢

转载自blog.csdn.net/xiaolyuh123/article/details/103290620
今日推荐