一次栈溢出问题的排查StackOverflowError

编程

栈溢出的原因

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

  1. 是否有递归调用,循环依赖调用
  2. 是否有大量循环或死循环
  3. 全局变量是否过多
  4. 局部变量过大,如:数组、List、Map数据过大

问题现象

我们一个很老的接口(近一年没有动过)在线上运行一段时间后报了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对象,直接提示了栈溢出:

通过上图我确认我找对了位置,然后根据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 的全部内容, 来源链接: utcz.com/z/511192.html

回到顶部