排查一次莫名其妙的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(),結局就是一個無限遞歸。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章