这篇文章主要讲解了“JavaSystem#exit无法退出程序的问题如何解决”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“JavaSystem#exit无法退出程序的问题如何解决”吧!有朋友碰到了一个情况:java.lang.System#exit无法退出应用程序。我听到这种情况的时候是感觉很惊奇的,这函数还能不起作用?这就好奇不已了呀接着,朋友继续给出了他的场景描述:在Dubbo应用连接注册中心的时候,如果连接(超时)失败,期望调用System#exit退出应用程序,但是程序并没有按期望退出,JVM进程还存在与此同时,如果把执行System#exit的代码放到另一个线程,程序可以按期望退出,JVM进程结束用伪代码描述如下:朋友面临的场景比伪代码描述的情况复杂的多,但所面临的本质问题是一样的。更一般化地问题,在Dubbo的org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry构造函数中,直接执行System.exit(1);
程序无法退出,放在异步线程中执行却可以按期望退出即:这就更令人惊奇了!要找出问题产生的原因,首先得有一些预备知识,否则会茫然无措,感觉无从下手java.lang.System#exit 方法是Java提供的能够停止JVM进程的方法该方法被触发时,JVM会去调用Shutdown Hook(关闭勾子)方法,直到所有勾子方法执行完毕,才会关闭JVM进程由上述第2点猜测:是否存在死循环的勾子函数无法退出,以致JVM没有去关闭进程?举个例子:如上,在main方法里先注册了一个shutdown hook,该勾子函数是个死循环,永远也不会退出,每3秒打印一次”closing…”接着执行System.exit(0);
方法,期望退出JVM进程before exit…
closing…
closing…
closing…
closing…
closing……结果是控制台不断打印”closing…”,且JVM进程没有退出原因正是上述第二点储备知识提到的:JVM会等待所有勾子执行完毕之后,才关闭进程。而示例中的shutdown hook 永远也不会执行完毕,因此JVM进程也不会被关闭尽管有了储备知识,仍然很疑惑:如果存在死循环的shutdown hook,那么System.exit
无论是在主线程中调用,还是在异步线程中调用,都应该不会关闭JVM进程;反之,如果不存在死循环的shutdown hook,无论是在哪个线程调用,都应该会关闭JVM进程。为什么在背景的伪代码中,却是因为不同的调用线程执行System.exit
,导致不一样的结果呢?这时候只好想办法,看看shutdown hook们都在偷摸干啥事,为什么未执行完毕,以致JVM进程不能退出恰好对Dubbo的源码也略有研究,很容易就找到org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry的构造函数,并在其中加上一行代码,如下所示,改完之后重新编译源码,并引入自己的工程中进行Debug注:本次使用的Dubbo版本为2.7.6启动工程,熟悉Dubbo的朋友应该会知道,应用启动的过程中会去注册中心(这儿是Zookeeper)注册或者订阅,因为启动的是消费者,因此应用会尝试连接注册中心Zookeeper,会走到ZookeeperRegistry
的构造函数,由于构造函数第二行是新增的代码System.exit(1);
按照背景的说法,JVM不会退出,且会卡死,这时候,借助IDEA的”快照”功能,可以”拍”下Java线程栈的运行情况,功能上相当于执行jstack
命令从线程栈中看出一个可疑的线程:DubboShutdownHook从名字上可以看出是一个Dubbo注册的一个shutdown hook,其主要目的是为了关闭连接、做一些资源的回收等工作从图中也可以看出,线程阻塞在org.apache.dubbo.registry.support.AbstractRegistryFactory
第83行从代码中很显然可以看出,因为获取不到锁,因此线程阻塞在第83行,等待获取锁,也就是说,有别的线程持着这把锁,但还没释放,DubboShutdownHook不得不等待着通过IDEA,查看有哪些地方获取了这把锁,如下,找到了会获取锁如此,System.exit无法退出JVM进程的问题总算真相大白了:1.Dubbo启动过程中会先获取锁,然后创建registry与注册中心进行连接,在ZookeeperRegistry中调用了java.lang.System#exit方法,程序转而执行”唤起shutdown hook”的代码并阻塞等待所有勾子函数执行完毕,而此时,之前持有的锁并没有释放2.所有勾子函数(每个勾子函数都对应一个线程)被唤醒并执行,其中有一个Dubbo的勾子函数在执行的过程中,需要获取步骤1中的锁,由于获取锁失败,就阻塞等待着3.由于1没有释放锁的情况下等待2执行完,而2的执行需要等待1释放锁,这样就形成了一个类似”死锁”的场景,因此也就导致了程序卡死,而JVM进程还存活的现象。之所以称为”类似”死锁,是因为1中执行System.exit的线程,也即持有锁的线程,永远不会走到释放锁的代码:一旦程序进入System.exit的世界里,就像进了一个单向虫洞,只能进不能出,如果勾子函数执行完毕,JVM进程接着就会被关闭,不会有机会再释放锁那么,为什么在异步线程中执行System.exit
,却能够正常退出JVM?那是因为:“唤起shutdown hook”并阻塞等待所有勾子函数执行完毕的线程是其它线程(此处假设是线程A),该线程在阻塞时并未持有任何锁,而主线程会继续往下执行并接着释放锁。一旦锁释放,Shutdown hook就有机会持有该锁,并且执行其它资源的回收操作,等到所有的shutdown hook执行完毕,A线程就能从阻塞中返回并执行halt
方法关闭JVM,因此能够正常退出JVM进程深入学习以上是对java.lang.System#exit 无法退出程序问题的分析,来龙去脉已经阐述清楚,受益于对Dubbo源码的了解以及正确的排查思路和排查手段,整个问题排查过程其实并没有花太多时间,但可以趁着这个机会,把java.lang.System#exit
系统学习一下,或许会对以后问题排查、基础组件设计提供一些思路System#exitTerminates the currently running Java Virtual Machine. The argument serves as a status code; by convention, a nonzero status code indicates abnormal termination.
This method calls the exit method in class Runtime. This method never returns normally.
The call System.exit(n) is effectively equivalen免费云主机域名t to the call:
Runtime.getRuntime().exit(n)这个方法实现非常简单,是Runtime#exit
的一个简便写法,其作用是用来关闭JVM进程,一旦调用该方法,永远也不会从该方法正常返回:执行完该方法后JVM进程就直接关闭了。入参status取值分两类:0值与非0值,0值意味着正常关闭,非0值意味着异常关闭。传入0值[有可能]会去执行所有的finalizer方法,非0值则一定不会执行(都不正常了,还执行啥finalizer呢?)。这儿提及[有可能]是因为,默认并不会执行finalizers,需要调用java.lang.Runtime#runFinalizersOnExit
方法开启,而该方法早被JDK标识为Deprecated,因此通常情况下是不会开启的接着看java.lang.Runtime#exit
,可以看到,最终调用的是Shutdown.exit(status);
,该方法是个包级别可见的方法,外部不可见java.lang.Shutdown#runHooks
有两个点需要注意,第一点MAX_SYSTEM_HOOKS(hooks)
这个并不是我们注册的shutdown hooks,而是按顺序预定义的系统关闭勾子,目前JDK源码(JDK8)预定义了三个:Console restore hookApplication hooksDeleteOnExit hook其中,Application hooks才是我们应用程序中主动注册的shutdown hook。在java.lang.ApplicationShutdownHooks
类初始化时,会执行static代码块,并在其中注册了Application hooks其次要注意的点是,给hook变量赋值的时候进行了加锁一般而言,给局部变量赋值是不需要加锁的,因为局部变量是栈上变量,而线程栈之间数据是隔离的,不会出现线程安全的问题,因此不需要靠加锁来保证数据并发访问的安全性。而此处加锁也并非为了解决线程安全问题,其真正的目的在于,通过Happens-Before规则来保证hooks的内存可见性:An unlock on a monitor happens-before every subsequent lock on that monitor。如果不加锁,有可能导致从hooks数组中读取到的值并不是内存中最新的变量值,而是一个旧值上面是读取hooks数组给hook变量赋值,为了满足HB(Happens-Before)
原则,需要确保写操作中同样对hooks变量进行了加锁,因此我们看一下写hooks数组的地方,如下:写操作确实加了锁,这样才能让接下来的读操作的加锁行为满足HB原则由于篇幅原因,就不展开具体的HB介绍,相信了解过HB原则的朋友一下就能明白其中的原理这个点个人感觉很有意思,因为锁的作用不单是为了保证线程安全,还可以用来做为内存通信、保证内存可见性的手段,因此可以当作面试的一个点,当下次面试官问到:你写的代码中用过锁(synchronized)吗?什么场景用到锁?都集群部署了,单机锁还有意义吗? 我们就可以回答:为了保证内存的可见性,balabalaba所以你瞧,这个点其实也给我们设计基础组件带来很大的启发,synchronized在当今集群、分布式环境下并非一无是处,总有合适的地方在等待着它发挥光和热注:JDK源码中真处处是宝藏,很多地方隐藏着巧妙而不可缺少的设计在给hook变量赋值之后,就执行 if (hook != null) hook.run();
,其中会执行到Application hooks,即上面提到的在ApplicationShutdownHooks
类初始化时注册的勾子,勾子内部调用了java.lang.ApplicationShutdownHooks#runHooks
方法上面的hooks才是应用程序真正注册的shutdown hook,由源码可以看出,每一个hook都对应着一个thread,且调用了它们的start方法,即开启thread,意味着shutdown hook是并发、无序地执行接着,唤起shutdown hook的线程,会通过死循环和join死等到所有关闭勾子都执行完毕,且忽略任何唤醒异常。也即是说,如果勾子们不执行完,唤醒线程是不会离开的等所有的Application hooks执行完毕,接下来会执行DeleteOnExit hook(如果存在),等所有system hooks执行完毕,也基本意味着sequence方法执行完毕,接下来就执行halt方法关闭JVM虚拟机这里额外还有一个知识点,上文只是提了一嘴,可能会容易忽略,此处拿出来解释一下:执行java.lang.System#exit
永远也不会从该方法正常返回,也即是说,即便System#exit
后边跟着的是finally,也不会执行 。一不注意就容易掉坑里java.lang.Runtime#addShutdownHook聊完System#exit
方法,接着来聊聊注册shutdown hook的方法。该方法本身实现上很简单,如下示:需要注意的是,注册的关闭勾子会在以下几种时机被调用到程序正常退出最后一个非守护线程执行完毕退出时System.exit方法被调用时程序响应外部事件程序响应用户输入事件,例如在控制台按ctrl+c(^+c)程序响应系统事件,如用户注销、系统关机等除此之外,shutdown hook是不会被执行的Shutdown hook存在的意义之一,是能够帮助我们实现优雅停机,而优雅停机的意义是:应用的重启、停机等操作,不影响业务的连续性以Dubbo Provider的视角为例,优雅停机需要满足两点基本诉求:Consumer不应该请求到已经下线的Provider在途请求需要处理完毕,不能被停机指令中断Dubbo注册了Shutdown hook,JVM在收到操作系统发来的关闭指令时,会执行关闭勾子在勾子中停止与注册中心的连接,注册中心会通知Consumer某个Provider已下线,后续不应该再调用该Provider进行服务。此行为是断掉上游流量,满足第一点诉求接着,勾子执行Protocol(Dubbo相关概念)的注销逻辑,在其中判断server(Dubbo相关概念)是否还在处理请求,在超时时间内等待所有任务处理完毕,则关闭server。此行为是处理在途请求,满足第二点述求因此,一种优雅停机的整体方案如下:感谢各位的阅读,以上就是“JavaSystem#exit无法退出程序的问题如何解决”的内容了,经过本文的学习后,相信大家对JavaSystem#exit无法退出程序的问题如何解决这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是百云主机,小编将为大家推送更多相关知识点的文章,欢迎关注!
这篇文章主要介绍“Go怎么快速实现驱动层流量抓包”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Go怎么快速实现驱动层流量抓包”文章能帮助大家解决问题。 一、驱动抓包应用层抓包我们可以使用Fiddler、Httpdebu…
免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。