现象
线上正常运行程序,本地测试加了log.warn(xxxx)日志后直接报StackOverflowError 异常。
web.xml 中配置的error_page页面,后台代码并没有具体报出来。
分析
栈内存是线程私有,尝试是否能够从线程堆栈信息查处具体原因
# 查看java进程
jps -l
76017 sun.tools.jps.Jps
48469
59846 org.apache.catalina.startup.Bootstrap
75358 org.jetbrains.jps.cmdline.Launcher
# dump出线程队栈信息
jstack -l 59846 >> /Users/xxx/Desktop/dump.log
可惜占内存溢出异常时间太短,很难在正在异常时间正好dump出线程堆栈信息
尝试加大栈内存,延长栈异常时间
重新启动项目加上jvm参数,这里将栈内存增加到100M,同时加上打印GC详情信息
-XX:+PrintGCDetails -Xss100M
重新运行异常代码,日志如下,可以看到栈内存溢出前,一直尝试做fullgc,持续了大约5~8秒
[GC (Allocation Failure) [PSYoungGen: 889056K->81675K(953344K)] 2710703K->1903340K(3749888K), 0.1361416 secs] [Times: user=0.06 sys=0.55, real=0.14 secs]
[GC (Allocation Failure) [PSYoungGen: 629515K->281504K(972800K)] 2451180K->2179106K(3769344K), 0.1584534 secs] [Times: user=0.21 sys=0.55, real=0.15 secs]
[GC (Allocation Failure) [PSYoungGen: 829344K->282720K(952320K)] 2726946K->2450456K(3748864K), 0.2698855 secs] [Times: user=0.36 sys=1.07, real=0.27 secs]
[GC (Allocation Failure) [PSYoungGen: 837728K->290944K(966144K)] 3005464K->2729323K(3762688K), 0.2007395 secs] [Times: user=0.65 sys=0.64, real=0.20 secs]
[Full GC (Ergonomics) [PSYoungGen: 290944K->0K(966144K)] [ParOldGen: 2438379K->955318K(2184704K)] 2729323K->955318K(3150848K), [Metaspace: 103144K->103144K(1142784K)], 0.8445731 secs] [Times: user=1.65 sys=1.29, real=0.85 secs]
[GC (Allocation Failure) [PSYoungGen: 555008K->291040K(974336K)] 1510326K->1246358K(3159040K), 0.1659465 secs] [Times: user=0.90 sys=0.12, real=0.16 secs]
[GC (Allocation Failure) [PSYoungGen: 870624K->304192K(982528K)] 1825942K->1533716K(3167232K), 0.2576112 secs] [Times: user=0.93 sys=0.72, real=0.25 secs]
[GC (Allocation Failure) [PSYoungGen: 883776K->304384K(993280K)] 2113300K->1820275K(3177984K), 0.2969893 secs] [Times: user=1.07 sys=0.67, real=0.30 secs]
[GC (Allocation Failure) [PSYoungGen: 895744K->309952K(993792K)] 2411635K->2112280K(3178496K), 0.3110348 secs] [Times: user=1.20 sys=0.67, real=0.31 secs]
[Full GC (Ergonomics) [PSYoungGen: 309952K->0K(993792K)] [ParOldGen: 1802328K->2060148K(2796544K)] 2112280K->2060148K(3790336K), [Metaspace: 103144K->103144K(1142784K)], 1.0683581 secs] [Times: user=3.92 sys=0.06, real=1.07 secs]
2022-07-17 20:51:27.481 [http-nio-8081-exec-3] ERROR
在异常溢出前继续dump出线程堆栈信息,如下图,这是就可以清楚的定位到哪个对象产生的问题,Order.toString()和
MException.toString()两个类一直交替打印,明显就是对象循环依赖,toString() 方法无限被调用,导致栈内存溢出。
toString() 方法是log.warn() 调用的,toString()方法是lombok @Data 自动生成的。
JNI global references: 43150
2022-07-17 19:27:32
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.311-b11 mixed mode):
"http-nio-8081-exec-7" #63 daemon prio=5 os_prio=31 tid=0x00007ff72518b000 nid=0x15843 runnable [0x0000000365a7c000]
java.lang.Thread.State: RUNNABLE
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.Order.toString(Order.java:19)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.MException.toString(MException.java:11)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at java.util.AbstractCollection.toString(AbstractCollection.java:462)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.Order.toString(Order.java:19)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.MException.toString(MException.java:11)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at java.util.AbstractCollection.toString(AbstractCollection.java:462)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.Order.toString(Order.java:19)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.MException.toString(MException.java:11)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at java.util.AbstractCollection.toString(AbstractCollection.java:462)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.Order.toString(Order.java:19)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.MException.toString(MException.java:11)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at java.util.AbstractCollection.toString(AbstractCollection.java:462)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.oncodsp.boss.pojo.Order.toString(Order.java:19)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
...
解决办法
1)重写对象的toString()
2)避免对象的循环依赖,本次选择的方法,从业务角度分析,Order中包含了MException后,没必要再MException在里包含Order信息
折腾了很久,上面的查找方法很麻烦,重新想一下栈内存溢出后台为啥没有打印具体信息,结果发现代码被try catch了,catch里捕捉的是Exception 对象,查看一下StackOverflowError 类,该类属于Error子类,不属于Exception子类。所以将捕捉异常范围再大一点,直接捕获Throwable 异常,则可以直接后台打印出异常信息