一个小小的常驻的Java程序,它只是很简单地做一些获取工作,但一些小细节可以令它极度容易成为一个僵尸进程。
症状:
在程序日志中看到抛出NullPointerException,之后没有日志了,也没有数据了,但进程还在,变成了僵尸进程。
分析:
抛出异常的地方在下面函数的return result;
@Override
public long getLoginMaxTimestamp(long start) {
Long result = null;
//按照方式一获取LoginMaxTimestamp,忽略SQLException
try {
result = this.getMaxTimestamp(System.currentTimeMillis(), MysqlDBUtil.getLoginStatExtConnection(), LOGIN_MAXTIMESTAMP_SQL);
} catch (SQLException e) {}
if(start<System.currentTimeMillis()){
if(result==null || result==0){
//按照方式二获取LoginMaxTimestamp,忽略SQLException
try {
result = this.getMaxTimestamp(start, MysqlDBUtil.getLoginStatExtConnection(), LOGIN_MAXTIMESTAMP_SQL);
} catch (SQLException e) {}
}
if(result==null || result<=start){
//按照方式三获取LoginMaxTimestamp,忽略SQLException
try {
result = this.getMaxTimestamp((start+System.currentTimeMillis())/2, MysqlDBUtil.getLoginStatExtConnection(), LOGIN_MAXTIMESTAMP_SQL);
} catch (SQLException e) {}
}
}
return result;
}
这里显然是因为result是null,导致JVM无法拆箱抛出NullPointerException。同时说明了上面三种方式都无法获取到数据。
那为什么程序会变成了僵尸进程?一直往上看调用方就知道了。
调用getLoginMaxTimestamp的是diffLogin:
protected static boolean diffLogin(boolean local){
long startMills = dcDao.getSavedTimestamp(DcTimestampDao.LOGIN_KEY);
// 这里调用的getLoginMaxTimestamp
long endMills = TimeEnum.Minute.getFormat(dao.getLoginMaxTimestamp(startMills)-TMP_MINUTE_UNBACK);
if(0==startMills){
startMills = endMills-ONCE_MINUTE_BREAKBACK*TimeEnum.Minute.getMills();
}
//下面省略...
调用diffLogin的是main函数:
public static void main(String[] args) throws ParseException{
String boolStr = "true";
if(null!=args && 1==args.length){
boolStr = args[0];
}
final Boolean local = new Boolean(boolStr);
// executor是ExecutorService的一个实例
// ONCE_SLEEP是一个常量,它的值是10
// 这里是提交一个Runnable到executor
// Runnable里面做一次diff,然后sleep 10毫秒,再重新提交自己到executor
executor.execute(new Runnable(){
@Override
public void run() {
diffLogin(local);
try {
Thread.sleep(ONCE_SLEEP);
} catch (InterruptedException e) {
}
executor.execute(this);
}
});
executor.execute(new Runnable(){
@Override
public void run() {
diffChannel(local);
try {
Thread.sleep(ONCE_SLEEP);
} catch (InterruptedException e) {
}
executor.execute(this);
}
});
}
看到这里就应该明白了。因为NullPointerException没有被捕获,Runnable没有再次提交自己,所以程序没有再进行下去。虽然主线程早就结束了,但是ExecutorService的线程池还在,所以进程还存活着,而且一直没有工作,成为了僵尸进程。
总结:
- 拆箱之前最好判断一下是不是
null,否则就干脆返回类类型; - 函数
getLoginMaxTimestamp没有覆盖到异常情况,异常情况可以选择抛异常或者返回特殊值(其实,说不定他是知道会有这种情况的,只是懒得写,直接让它抛NullPointerException就算了); ExecutorService在异常情况下shutdown;
其实,他只要做到在异常情况下调用ExecutorService的shutdown,那么TA就不会成为一个僵尸进程了。遵循FastFail原则,出错就让它挂掉,再监控进程,出问题就发出报警,那么这个世界就会美好很多了……
要写好Java程序真不容易啊……

本文通过一个具体的Java程序案例,分析了程序如何因未捕获的NullPointerException异常而变成僵尸进程的问题。涉及异常处理不当、线程池管理不善等问题,并提出了相应的解决措施。
4960

被折叠的 条评论
为什么被折叠?



