前言
昨天在工作中遇到一个很奇怪的空指针问题,接下来看下问题的定位到解决。
问题出现
问题发生在一个平时都很稳定的微服务中,负责的责任田为整个产品的框架组,该服务也是框架服务中的核心之一,因此直接被测试提了严重单,该微服务已经接近三个月没有过新代码合入。报错日志如下:
从日志中可以看到发生在Job.getJobId中,不用多想,肯定是该job对象为空了。接着贴一下问题发生处的代码:
public static Job newJob(DirectedGraph<Job> graph, ServiceJob serviceJob, Resource resource)
{
Job job = new Job();//1
job.setGraph(graph);
job.setServiceJob(serviceJob);//2
job.setResource(resource);//3
return job;
}
该段代码大致作用是将上层传入的serviceJob塞到一个新new出来的job中(虽然我现在还不知道为什么不直接用构造函数,问了原作者,说下周一看看。。。),然后可以确定serviceJob肯定不为空(前后都有判空),而且仔细看下日志,报错的位置居然发生在hashcode方法中,接着贴一下hashcode方法:
@Override
public int hashCode()
{
return new HashCodeBuilder().append(getJobId()).append(resource.getResourceLabel()).toHashCode();
}
终于找到问题发生在哪儿了,在hashcode方法中引用了getJobId方法,此时job为空的话确实会报空指针,但是!看到上一段代码,在2处job就不应该为空了,因此问题只能发生在第一步,再但是!为什么new一个job对象会去调用hashcode方法呢???于是我们同事三个傻白萌就开始排查环境问题了,甚至研究了一些比较悬学的点(比如jre版本。。在问题发生前jre版本为1.3.40,而更新后为1.3.41;以及平台代码是否有更新。。。),在一无所获之后,盯着屏幕发了会呆,突然看到了调用栈中的如下日志:
。。。这个是昨天平台加入的鹰眼检测方法,大致就是一个检测空指针的类,开始我们都觉得空指针抛在我们这儿,肯定是我们的问题,但查看这个类的源码后才发现,是由于这个检测类引起的,大致就是在我们服务的启动脚本中会去调用鹰眼检测方法,检测是否存在可能造成空指针的代码,偏偏这个方法写的又有问题,在检测到空指针异常时没有catch住,当异常抛出后,又会起一个线程去检测,然后造成了一个死循环,导致内存溢出,而在内存溢出的时候,平台又有另外一个机制,对每一个新new出来的对象去比对hashcode,看是否可以回收。。。因而去调用了这个hashcode方法,此时job确实还没有完成构造,因此出现了上述问题。。。
总结
1、虽然这个问题的主要责任在于平台的代码,但是我相信这个newJob方法肯定是不规范的,如果在构造函数中赋值,完全可以规避这个问题,但是为什么要这样写而不用构造函数,还要去咨询下原作者,看是否有其他用意。
2、在发生问题时,一定要细心!不知道大家有没有这样的习惯,看异常日志的调用栈时,看到第三方的方法后就不会再往前看了。。。因此才会浪费这么长时间。