一次Java空指针异常排查经历

同理先说背景:我们的一个注册中心服务,多节点部署,客户端会hash到一个节点,启动定时任务与之通信拉取配置。突然有业务线说客户端大量报超时异常。

首先当然赶紧让业务线把异常日志发给我。先分析了一下日志,找到了对应的超时异常,根据堆栈信息分析出是拉取配置任务大量连续超时,再定位到是哪一台服务。

因为之前线上出现过超时情况,是因为代码效率太低,每个拉取配置的请求要处理将近750ms,导致了请求队列堆积。优化后到70ms,为了防止再出问题加了对请求队列监控的日志。登到服务器上发现请求队列没有堆积,但是发现了很多npe。赶紧登陆到别的节点,确认是这个节点的服务器有问题还是有代码逻辑问题,发现所有的节点都在报npe。接着确认这个问题是只有这个业务线有问题,还是所有业务线共性问题,好确定是业务线的客户端问题还是注册中心的问题,很不幸所有业务线都有问题。因为这个定时任务不会影响业务线正常业务运行,所以业务线没有关注。

grep一遍线上日志,发现线上日志竟然没有打堆栈信息!!!幸好有个关键字,可以知道是哪个方法抛的npe,只能从方法入口开始撸一遍代码,代码结构大概如此:

public class Demo {

	public void demo(){
		try{
			doSomeThing();
		}catch(Exception e){
			log.error("xxxxx"+e);//就是这里没打堆栈信息,为什么是个+号啊...
		}
	}

	private XXObject doSomeThing() {
		try{
			/**
			 * do something
			 */
			return XXObject.success();
		}catch(Exception e){
			return XXObject.fail(e.getMessage().getBytes());
		}
	}
}

这根本就不可能抛出来npe,除非doSomeThing()的catch里抛出来npe。根据经验这完全不可能啊。没办法加上日志,把堆栈信息打印出来,打包上线,等着再报错。

npe没出来,开始出现另外的ClassCastException异常了,本着快速解决问题,先把这个解决了。再打包上线,发现问题解决。

完美解决,撒花。

你以为我就想说这些吗,错了。下面才是重点。

这个npe一直在我头上盘旋,搅得我一下午不得安宁,我一定得把这个npe抓出来。

先把代码抽象,简化模拟当时的场景,简化代码如下:

public class ExceptionTester {
	public static void main(String args[]) {
		try {
			test();
		} catch(Exception e) {
			System.out.println(e.getMessage().getBytes());
		}
	}
	private static void test() throws Exception {
		A a1 = new A();
		B b1 = (B) a1;
	}
	static class A {
	}

	class B extends A {
	}
}

唯一一个能使代码抛出npe的就只有

扫描二维码关注公众号,回复: 10971419 查看本文章
e.getMessage().getBytes()

这句话了,那么什么情况下e.getMessage()会返回null呢。本地跑测试完全没有问题。看了遍ClassCastException的jdk文档也没有特殊说明什么场景会返回null。

只好比对一下本地和线上的JDK有什么不同,发现都是完全一样:

java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)

在毫无头绪的时候,又重新梳理了一遍问题,先是大量npe可以确定是e.getMessage()返回了null,重启服务后报ClassCastException,修复ClassCastException之后问题解决。那么在大量npe之前是否也报了ClassCastException,重新查日志,发现确实有大量ClassCastException。那么问题的关键难道是大量的ClassCastException,导致了e.getMessage()返回了null。talk is cheap,show me the code。写模拟程序,跑一遍:

public class ExceptionTester {
	public static void main(String args[]) {
		int total = 0;
		int error = 0;
		for (int i = 0; i < 200000; i++) {
			try {
				test();
			} catch(Exception e) {
				total++;
				if (e.getMessage() == null) {
					error++;
				}
			}
		}
		System.out.println(total);
		System.out.println(error);
	}
	private static void test() throws Exception {
		A a1 = new A();
		B b1 = (B) a1;
	}
	static class A {
	}

	class B extends A {
	}
}

惊人发现,error竟然不是0,而且还很大。真的是连续抛出一个异常,导致了e.getMessage()返回了null。兴致冲冲准备去给JDK提个bug,发现竟然早就有人提过了:NullPointerException with no stack trace

大致查了一下原因,sun给的解释是:

The compiler in the server VM now provides correct stack backtraces for all “cold” built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag: -XX:-OmitStackTraceInFastThrow.

因为现在Hot Spot VM 采用了 JIT compile 技术,所以为了性能考虑,如果连续抛出一个异常,这个方法就会重新编译。重新编译后,编译器可能会采用预先分配的没有堆栈信息的异常。想不用的话,在JVM启动参数里加上

-XX:-OmitStackTraceInFastThrow即可。我实际测了一下,加上参数后,连续抛出20w个异常,耗费时间会增加两倍以上,果然性能确实是有优化。

后来到网上搜了一下,问题相关的描述还是很多的,虽然没提成bug,但是这一次排查经历也收获很多。

第一点:日至一定要打全,异常处理一定要做好。

第二点:经验主义害人,遇到问题模拟一遍最好。

第三点:有问题先到网上搜一遍,可能早有人遇到和解决了。

相关的描述比较清楚的文章有如下:

http://jawspeak.com/2010/05/26/hotspot-caused-exceptions-to-lose-their-stack-traces-in-production-and-the-fix/

https://stackoverflow.com/questions/40502576/java-lang-classcastexception-with-null-message-and-cause

发布了45 篇原创文章 · 获赞 21 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/ly262173911/article/details/84841950
今日推荐