一次栈溢出问题的排查StackOverflowError
栈溢出的原因
在解决栈溢出问题之前,我们首先需要知道一般引起栈溢出的原因,主要有以下几点:
- 是否有递归调用,循环依赖调用
- 是否有大量循环或死循环
- 全局变量是否过多
- 局部变量过大,如:数组、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(
...)))))));
当层级达到一定数量后,我们再调用这个list
的get
方法时就会发生调用链太长,进而将方法栈撑爆,出现栈溢出。到这里就找到了问题的根源。
解决方案
- 问题根源就是我们缓存了
MappedStatement.Builder
对象,我们去掉缓存后,代码恢复了正常。 - 我们不去新创建
MappedStatement
,直接修改原有MappedStatement
的sql语句,在原来sql语句后面加上limit ?,?
,最后分页信息通过参数传入。
倒推问题答案
- 为什么这个问题以前运行得好好的,直到几年后的今天才被发现?
这是因为我们以前这个服务发版很频繁,导致每次发版后这个装饰的层级被清空了。
- 为什么只有这一个接口出现了问题?
这是因为这个接口是使用分页查询接口中访问量最大的那个接口,所以它最先出现问题。
- 为什么线上只有那么一两台机器出现问题?
这是因为出问题的机器负载高一些,到时这些机器先出现问题。
总结
- 栈溢出的原因基本上就是我上面列举的那些,但是我们在编写程序的过程中都会有意识的避开这些问题,所以线上出现栈溢出的可能性很小,但是一旦出现就不好排查。我们需要静下心来慢慢分析,总会找到问题根源的,只是过程有点痛苦。
- 在没有完全了解Mybatis运行原理的情况下,不建议做Mybatis的插件开发。
- 没动过代码不代表系统就不会出现问题。
以上是 一次栈溢出问题的排查StackOverflowError 的全部内容, 来源链接: utcz.com/z/511192.html