排查一次莫名其妙的StackOverflow

一、现象

某dubbo微服务启动后,只要被消费者调用,就发生以下错误

com.alibaba.dubbo.common.logger.slf4j.Slf4jLogger 74 error -  
[DUBBO] Got unchecked and undeclared exception which called by 192.168.1.101. 
service: com.xxx.facade.payment.service.IPaymentService, 
method: getAccountBalance, exception: java.lang.StackOverflowError: null, 
dubbo version: 2.5.3, current host: 127.0.0.1 java.lang.StackOverflowError: null
下略

这简直是个莫名其妙的事情,这一次只是追加了一个很简单的,居然造成其他既存接口出问题,而且还是StackOverflowError。

阅读日志,又发现其他很多既存接口也出现了同样错误。

(黑人问号啊……)

二、临时解决

  • 做初步调查的时候,因为 StackOverflowError 太少见,完全没有头绪,有点慌。

  • 先将本次修改的代码全部revert,恢复到上一次发版的状态,经验证工作正常,这样就把出错原因限定在了这次修改的代码里,而不是运行环境。

  • 接下来本来想做的事情是把这次添加的代码再一点一点恢复回来,逐步锁定出错的代码块。

  • 幸好,因为改的地方太零散了,这么做起来太花时间,犯懒,实在不想这么做。

  • 所以硬着头皮重新读日志——之前看到这个错误的时候,其实给了自己一个不好的暗示:这种错误都是系统底层的问题,这一堆天书一样的日志根本不可能看懂。不过,仔细看的时候发现还是很清晰的

2019-08-15 17:20:57.422 INFO  com.xxx.commons.logs.AspectLog 44 before - [before()]com.xxx.facade.payment.service.impl.PaymentServiceImpl.getAccountBalance(), parametes:[12,13400518650]
2019-08-15 17:20:57.497 ERROR com.alibaba.dubbo.common.logger.slf4j.Slf4jLogger 74 error -  [DUBBO] Got unchecked and undeclared exception which called by 192.168.1.101. service: com.xxx.facade.payment.service.IPaymentService, method: getAccountBalance, exception: java.lang.StackOverflowError: null, dubbo version: 2.5.3, current host: 127.0.0.1 java.lang.StackOverflowError: null
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:156) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:52) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at com.xxx.facade.fuiou.constant.FuiouConstant$$EnhancerBySpringCGLIB$$fb956bdd.toString(<generated>) ~[phj-facade-payment-1.0-SNAPSHOT.jar:1.0-SNAPSHOT]
	at com.xxx.commons.logs.AspectLog.before(AspectLog.java:40) ~[phj-common-1.0-SNAPSHOT.jar:1.0-SNAPSHOT]
	at sun.reflect.GeneratedMethodAccessor83.invoke(Unknown Source) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_101]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_101]
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:611) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.aspectj.AspectJMethodBeforeAdvice.before(AspectJMethodBeforeAdvice.java:43) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:51) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:52) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
	at com.xxx.facade.fuiou.constant.FuiouConstant$$EnhancerBySpringCGLIB$$fb956bdd.toString(<generated>) ~[phj-facade-payment-1.0-SNAPSHOT.jar:1.0-SNAPSHOT]
	at com.xxx.commons.logs.AspectLog.before(AspectLog.java:40) ~[phj-common-1.0-SNAPSHOT.jar:1.0-SNAPSHOT]
	at sun.reflect.GeneratedMethodAccessor83.invoke(Unknown Source) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_101]

  • 虽然有一堆鬼画符一样的springframework,但是有两行 com.xxx(公司名称) 啊
	at com.xxx.facade.fuiou.constant.FuiouConstant$$EnhancerBySpringCGLIB$$fb956bdd.toString
	at com.xxx.commons.logs.AspectLog.before
  • 因为这次确实在 FuiouConstant 这个类里面随手追加了一个 toString() 方法(为了在日志里面出详细信息,方便追踪)。所以试着把这个toString()删掉试验了一下,工程恢复正常。

  • 至此,至少知道了表面上引发错误的原因,问题解决。以下开始追查根本原因。

三、原因追查

3.1、什么情况下会发生StackOverflowError?

因为StackOverflowError太特殊了,首先能够肯定的,是发生了深度嵌套调用(最常见的是递归),因为只有在线程请求的栈深度过大,超过了规定值以后,才会抛出StackOverflowError错误——这是这种错误唯一可能发生的原因。

顺便说一句,栈会发生的另一种错误是OOM,在栈动态扩展空间,但是内存不足时发生。

3.2、一个错误的调查方向

因为以前见过toString()引发StackOverflowError的事例(参看“五、附录 5.1”一节)。

所以一上来就认为是 FuiouConstant 里面的成员变量与 FuiouConstant 本身形成了某种循环调用。

不过 FuiouConstant 里面的成员变量都是String,理论上无论如何也是不可能的。

但是这个观念根深蒂固,考虑到 FuiouConstant 里面变量的内容是通过 xml 注入进去的(读了propertiy文件),会不会是这样造成的影响?

去把 PropertyPlaceholderConfigurer 的源代码研究了半天,又拼命做各种试验,最后晕头转向没有任何进展。

3.3、光看不跑

最后想到,出错信息里面还提到了“com.xxx.commons.logs.AspectLog.before”,打开看了看代码,感觉一切正常。

甚至还改造了一下,多输出了点日志

com.xxx.common.phjservicepayment.PhjServicePaymentApplicationTests 47 getAccountBalance - ==== 用户余额查询 ====
com.xxx.commons.logs.AspectLog 41 before - ====com.xxx.facade.payment.service.impl.PaymentServiceImpl
com.xxx.commons.logs.AspectLog 42 before - ----getAccountBalance
com.xxx.commons.logs.AspectLog 48 before - [before()]com.xxx.facade.payment.service.impl.PaymentServiceImpl.getAccountBalance(), parametes:[11,15288900256]
com.xxx.commons.logs.AspectLog 41 before - ====com.xxx.facade.fuiou.constant.FuiouConstant
com.xxx.commons.logs.AspectLog 42 before - ----getMchntCd
com.xxx.commons.logs.AspectLog 41 before - ====com.xxx.facade.fuiou.constant.FuiouConstant
com.xxx.commons.logs.AspectLog 42 before - ----toString
com.xxx.commons.logs.AspectLog 41 before - ====com.xxx.facade.fuiou.constant.FuiouConstant
com.xxx.commons.logs.AspectLog 42 before - ----toString
com.xxx.commons.logs.AspectLog 41 before - ====com.xxx.facade.fuiou.constant.FuiouConstant
com.xxx.commons.logs.AspectLog 42 before - ----toString

发现一直在跑FuiouConstant自身的toString(),倒是打消了之前toString嵌套调用的想法。

3.4、折腾半天快被气死

最后实在没辙了,差点要取内存的dump文件下来分析。

然后随手点了debug启动,想在输出日志时的切面里面观察变量

@Before(value = "execution(public * com.xxx.facade..*.*(..))" ) 
public void before(JoinPoint jp) { 
       String methodName = jp.getSignature().getName();   //获得方法名
       	
       	 String className = jp.getThis().toString();//获得类名
       	 Object[] args = jp.getArgs();  //获得参数列表
         if(args != null && args.length > 0) {
        	 String paraString = generateParaString(args);
        	 LOGGER.info("[before()]" + this.generateReadableString(className) + "." + methodName 
        			 + "(), parametes:" + paraString);
        	 
         } else {
        	 LOGGER.info("[before()]" + this.generateReadableString(className) + "." + methodName + "()");
         }
            
} 

通过debug发现,做公用方法的同事在获取类名的时候埋了一个坑:

String className = jp.getThis().toString()

JoinPoint的getThis()方法用于获取被切的对象,但是,后面带了一个 .toString()…………

这获取的实际上是实例转然后string,不是类名啊!

所以,被切的实例(public * com.xxx.facade….)里面,只要被调用toString方法,就是进入无出口的递归调用里面,这里必然StackOverflow!

实际上,这段代码在刚刚开始调查错误的时候就看到了,但是没有debug一句一句跟着跑,跟着看,根本没意识到会出事。

四、总结(重要!)

总结1

发生稀奇古怪的错误以后,不要慌,镇静,然后强迫自己一条一条仔细读日志,哪怕觉得看不懂也要一条一条仔细读!

总结2

对于错误原因没有较大把握的时候,别瞎猜,或者说别被自己的经验带来的偏见误导。东一下西一下瞎猜瞎试的话,反而可能更浪费时间。出错位置能debug的话,一定要第一时间跟着代码跑一遍,比瞎猜强100倍。

五、附录——如何利用toString()方法触发 StackOverflowError

5.1、两个POJO中互相定义了对方类型的成员变量
  • 其实在一对多的模型中应该是见不到这种情况的,比如 borrow_info 和 borrow_invest ,borrow_invest 的定义中有一个 borrow_info 类的成员变量是合理的,而 borrow_info 中不会出现 borrow_invest ,没有意义。

  • 但是在一对一的关系中 —— 比如UML中定义的 1:1 关联 关系中,这种互相包含从逻辑关系上来说是合理的。举例:牛郎织女。但是这种关系定义在POJO中的话造成隐患

public class A {
    private int aValue;
    private B bInstance = null;

    public A() {
        aValue = 0;
        bInstance = new B();
    }

    @Override
    public String toString() {
        return "";
    }
}


public class B {

    private int bValue;
    private A aInstance = null;

    public B() {
        bValue = 10;
        aInstance = new A();
    }

    @Override
    public String toString() {
        return "";
    }

    public static void main(String[] args) {
        A obj = new A();
        System.out.println(obj.toString());
    }
}

运行B的main()方法以后

Exception in thread "main" java.lang.StackOverflowError
	at com.xxx.bootmint.genStackOverflow.B.<init>(B.java:10)
	at com.xxx.bootmint.genStackOverflow.A.<init>(A.java:9)
	at com.xxx.bootmint.genStackOverflow.B.<init>(B.java:10)
	at com.xxx.bootmint.genStackOverflow.A.<init>(A.java:9)
	at com.xxx.bootmint.genStackOverflow.B.<init>(B.java:10)
	at com.xxx.bootmint.genStackOverflow.A.<init>(A.java:9)
	at com.xxx.bootmint.genStackOverflow.B.<init>(B.java:10)
	at com.xxx.bootmint.genStackOverflow.A.<init>(A.java:9)
……
5.2、自己瞎写toString(),使用了this

这种情况在实际工作中应当是见不到的,一般都是使用IDE自动生成toString(),不应该有人手写吧

public class C {

    private Integer id;
    private String borrowName;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getBorrowName() {
        return borrowName;
    }

    public void setBorrowName(String borrowName) {
        this.borrowName = borrowName;
    }

    @Override
    public String toString() {
        return "borrow_info:" + this;
    }

    public static void main(String[] args) {
        C c = new C();
        System.out.println(c.toString());
    }
}

运行结果

Exception in thread "main" java.lang.StackOverflowError
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at com.xxx.bootmint.genStackOverflow.C.toString(C.java:26)
	at java.lang.String.valueOf(String.java:2994)
	at java.lang.StringBuilder.append(StringBuilder.java:131)
	at com.xxx.bootmint.genStackOverflow.C.toString(C.java:26)
	at java.lang.String.valueOf(String.java:2994)
	at java.lang.StringBuilder.append(StringBuilder.java:131)
	at com.xxx.bootmint.genStackOverflow.C.toString(C.java:26)
	at java.lang.String.valueOf(String.java:2994)
	at java.lang.StringBuilder.append(StringBuilder.java:131)
	at com.xxx.bootmint.genStackOverflow.C.toString(C.java:26)
……	

原因其实很简单,toString()方法中使用 “+” 号连接字符串和 this 的话,jvm会自动将 this 转变为 String 类型,那么就触发了this的toString(),然后新的toString()里面又要触发toString(),结局就是一个无限递归。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值