前段时间公司有个系统的进程总是无端退出,在客户那边好好的,在家里服务器上总是出现,并且出现的时间也没啥规律,固然最终查出来仍是有规律的,不过这个规律比较特别。你们看了后面的内容以后就明白了,真的很特殊!java
当同事找到个人时候,我第一反应是是否是进程Crash了,若是是crash,那一般状况下会有crash的日志,检查了一遍,什么日志都没有留下,固然有时候Crash了,JVM也确实也不会留下Crash日志,不过这个特别罕见了,绝大部分是人为操做了。shell
既然不是Crash,那是否是系统存在内存泄露,被OS Kill了,不过很快经过dmesg也排除了,由于没有看到任何kill的迹象。服务器
排除掉以上两个因素以后,接着立刻就怀疑是否有什么代码执行过System.exit,因而从新编译了一把JDK,在System的exit方法处打印了些日志,因而坐等奇迹的发生。微信
使人兴奋的是,进程真的消失了,但是使人遗憾的是,咱们埋点的日志并无出现。这让我再次陷入沉思。session
从日志看确实是调用了ShutdownHook函数
因而找到addShutdownHook源码的位置工具
再次翻了一下JDK的源码,除了正常退出,System.exit等以外还有哪些状况会调用这个Shutdown的Hook,因而将埋点埋到了Shutdown.runHooks方法里。spa
继续等待事情再次发生,果真没多久,当天下午又发生了,打印了以下日志日志
也就是说当时是收到了一个SIGHUP的信号,这个信号最终会让进程退出,JVM对这个信号确实是没有特殊处理的,所以没有咱们看到的Crash日志。code
那接下来就是要找到为何会收到这个信号了,这个信号又是谁发出的。
SIGHUP这个信号,最主要发生的场景是Shell终端关闭一个Session的时候会对该Session关联的进程发送一个SIGHUP信号,这个信号默认是会退出进程的。为此我还特意下载了ITerm2(我和同事都是mac,用的iTerm2的终端)的源码,还真找到了几个发送SIGHUP信号的代码
看名字PTYTask就能猜到了,这应该是一个seesion任务,因而进到代码里看到主要是有两个方法有发送SIGHUP信号给子进程,分别是dealloc和stop,其中stop会经过sendSignal函数给子进程发送SIGHUP信号。
对于我这种比较喜欢自虐的人,通常会想怎么就肯定这个就是我要找的代码,以前从没有写过Object-C的代码,想着是否有个相似java的jmap的工具可让我看内存里的对象的状况,然而临时没找到,不过偶然发现mac自带的Activity工具就能看到一些迹象,因而在Activity里找到了iTerm2进程,而后对其内存数据进行了采样,看可否抓到相似PTYTask. dealloc或者stop的调用栈,但是比较难模拟,由于存在时间差,点击采样的时候,很快就结束了,我还没来得及关闭session。在看采样报告的时候偶然看到了/usr/bin/sample的命令,原来Activity是采用这个命令进行采样的,因而摸索了一把,真能搞起来,采样的时间能够自定义,间隔是1ms一次,这样可让我有充足的时间来操做了,因而在采样开启以后,不断地开一个session,起一个进程,而后close,重复作了好几回,结束采样以后看采样的输出,还真的就抓到了PTYTask.stop的调用栈
这也就验证了我close session的时候确实会给对应的子进程发送SIGHUP的信号。
那到此为止咱们能够确认的是
那究竟是不是这种状况呢?
这个貌似不太可能,由于咱们的shell脚本里执行java的时候都会带上&,这样进程就会后台运行,不会出现这种session的问题。看了下脚本,确实是带上了&的,本身也模拟了几遍,在一个shell里调用带有&的java命令,关闭终端java进程并不会退出。难道是由于同事的终端配置和个人不同?后来要同事发了它的iTerm2里的seesion的配置给我看了下,和个人彻底同样,这就挺奇怪了。
因而再回过来看看以前的几回进程消失时候的日志(我要同事有保留),看到那几个进程退出的时间点,分别是
这几个时间点彻底没有规律看起来,不过我忽然想起2019-01-18那天是周五,同事是在上海出差,而17:34:30那会应该是快下班了,或者那会正好从上海赶车回杭州,因而问了下那个点同事是否是准备回杭州了,同事说那会确实合上电脑准备赶车回杭州,因而我接着问其余几个时间是否是差很少也是合上电脑的时间,结果还真是那么巧,既然这样,那说明咱们的判断方向是正确的了。
后面忽然发现同事修改过这个启动脚本,在脚本的最后加了行命令
java xxxx &cd $DIR_LOG && tail -f common-*.log
忽然想起一个问题来,父子进程的问题,假如说当咱们在shell里加了&跑的话,当父进程跑完以后,会直接将其挂到init进程下面,也就是你经过ps -ef去看它的父进程会是1号进程,可是若是原来的父进程没有跑完,那其父进程仍然会是原来的进程,因此当咱们执行这个脚本的时候,一直被执行tail等待日志输出,这样一来,若是咱们将当前窗口关闭了,那就意味着父进程会退出,而后给子进程发送SIGHUP的信号,因而致使java进程退出,可是若是咱们在tail日志过程当中ctrl c了,那java进程会过继给init进程,所以此时当咱们再关闭窗口的时候也不会给其发送SIGHUP的信号了。
通过验证果真如此,至此这个消失的进程终于获得了解释,嗯,好在不是咱们代码的问题,值得欣慰,毕竟咱们的代码是要在客户那边运行的,必需要保证好稳定性。
推荐阅读:
当心踩雷,一次Java内存泄漏排查实
跟Kafka学技术系列之时间轮
欢迎关注 PerfMa 社区微信公众号,体验专业JAVA分析工具