公共 类 正常关机测试 { 公共 空白 开始 ( ) {nruntime.getruntime(). Addshutdownhook(New Thread(()-> System.out.println("钩子函数执行完毕,这里可以关闭资源了。" ) )); 静态 void main (字符串[]参数) { new NormalShutdownTest().start() ; System.out.println("主应用程序正在执行,正常关闭。" );
输出结果为:
主应用程序正在执行,将正常关闭。 钩子函数执行完毕,这里可以关闭资源。
可以看到钩子函数的代码正常执行。如果在main函数中添加System.exit(0)代码,执行后的结果仍然是一样的。这说明JVM正常关闭时是支持优雅关闭的。
异常关闭
在这种情况下,JVM 异常关闭,我们尝试创建内存溢出。只需声明一个 500 MB 的数组并将 JVM 堆设置为最大 20 MB (-Xmx20M)。代码如下。
公共 类 OomShutdown测试 {( ) {nruntime.getruntime(). Addshutdownhook(New Thread(()-> System.out.println("钩子函数执行完毕,可以在这里关闭资源" ) )); 静态 void main (字符串[]参数) 抛出 异常{新 OomShutdownTest().start(); System.out.println("主应用程序正在执行,内存溢出已关闭。" ); 字节 [] b = 新 字节 [500 * 1024 * 1024 ]; } }
执行结果为:
主应用程序正在执行,内存溢出已关闭。 异常in 线程“main” java.lang.OutOfMemoryError:Java堆空间 位于tech.shuyi.javacode芯片.shutdownhook.OomShutdownTest.main(www.sychzs.cn: 13) 钩子函数执行完成,这里可以关闭资源
可以看到JVM抛出了OOM错误,但是hook函数仍然执行了。如果你自己在main函数中抛出RuntimeException,钩子函数仍然会被执行。有兴趣的朋友可以自己尝试一下。
强制关闭
这种情况下JVM会被强制关闭,我们可以使用Runtime.getRuntime().halt(1)来测试,代码如下。
公共 类 强制关机测试 { 公共 空白 开始 ( ) {nruntime.getruntime(). Addshutdownhook(New Thread(()-> System.out.println("钩子函数执行完毕,这里可以关闭资源了。" ) )); 静态 void main (字符串[]参数) 抛出异常{新 ForceShutdownTest().start(); System.out.println( ? }
执行结果:
主应用程序正在执行并被强制关闭。
可以看到钩子函数还没有被执行,所以JVM强制关闭不支持优雅关闭。
最佳实践看了上面的例子,看来优雅关机并没有那么复杂。事实上,如果优雅关闭使用不当,还可能会出现其他问题。以下是一些最佳实践原则,可帮助您充分利用优雅关闭!
仅注册1个钩子
我们都知道JVM可以注册多个hook,而一个hook本质上就是一个可以并发执行的线程 。那么很可能Hooks会相互依赖,从而导致依赖死锁。另外,多个hook操作同一个资源时,可能会由于资源竞争而出现死锁。因此,更好的办法是只注册一个hook,所有的资源释放都在这个hook中进行操作。
确保线程安全
因为钩子本质上是一个线程,所以 JVM 可以同时 执行多个钩子。 JVM不保证它们的执行顺序,因此需要保证钩子中的操作是线程安全的。当然,如果你只有一个钩子,你可以忽略这个提示。
不要做耗时的操作
在钩子中,不要做耗时的操作。因为当我们要关闭JVM时,用户肯定希望尽快关闭,所以钩子主要是用来关闭剩余资源的,不应该进行其他耗时的操作。
请勿注册或移除挂钩
在shutdown hook中,不能进行注册和移除hook的操作,否则JVM会抛出IllegalStateException。
不调用System.exit()操作
不能调用System.exit()操作,但可以调用Runtime.halt()操作。我认为这是因为调用System.exit()操作会导致循环进入钩子,导致死循环。
需要考虑的资源
除了上面需要考虑的代码操作外,我们还需要关注以下场景的处理:
池化资源的释放:数据库连接池、HTTP连接池、线程池。
隐形受影响资源的处理:Zookeeper、Nacos实例下线等
应用案例
Java提供的优雅关闭机制可以说是很多框架的基础。 spring 、Consul等中间件框架都利用了Java提供的优雅关闭机制。
Spring 正常关闭
比如Spring是基于Java语言开发的框架,那么它也必须依赖于JVM的ShutdownHook。 Spring 优雅关闭的代码位于 org.springframework.context.support.AbstractApplicationContext#registerShutdownHook
。代码如下图所示。
@Override public void registerShutdownHook () { 如果 (这个 .shutdownHook == null ) { //尚未注册任何关闭钩子。 这个 .shutdownHook = new 主题(SHUTDOWN_HOOK_THREAD_NAME) { @Override public 空 运行 () { 同步 (启动ShutdownMonitor) { doClose(); } } }; // 增加 ShutdownHook 钩子 Runtime.get运行时()。 addShutdownHook(这个 .shutdownHook); } }
可以看到Spring在registerShutdownHook()函数里,注册了一个关闭的钩子,钩子中调用了doClose()方法。
服务治理的缓慢
不管是Dubbo还是Spring Cloud的全方位服务框架,需要关注怎么能在服务停止前,先将服务提供者在注册中心进行反注册,然后在服务提供者停止,这样才能保证业务系统不会产生各种503、超时等现象。为了实现上面所说的效果,那么我们就必须关注优雅这件事情。
复活节彩蛋
我们都知道杀掉-15可以让JVM优雅关闭,那么我们是否可以监听特定的信号量,让程序执行特定的操作呢?比如:让JVM监听第12个信号量,然后打印一个log ,然后优雅关闭。
答案是当然!我们只需要使用Signal类并实现一个SignHandler类。实现代码如下:
公共 类 自定义关机测试 { ( ) {nruntime.getruntime(). Addshutdownhook(New Thread(()-> System.out.println("钩子函数执行完毕,这里可以关闭资源了。" ) )); 静态 void main (字符串[]参数) { //自定义信号kill 信号sg = 新 信号("USR2" ;@Override 公共 空隙 手柄 (信号signal) { System.out.println("收到信号量:" + signal.getName ( )); ); } );ic new CustomShutdownTest().start(); new CustomShutdownTest().start();正常关机。" ); try { Thread.sle ep(30000 ); catch (InterruptedException e) { e.printStackTrace(); } }
我们开始这个类后,让它休眠30秒,然后使用jps命令找到进程ID,然后运行kill -USR2 PID,如图在截图中。
然后您可以看到控制台上打印出以下消息:
主应用程序正在执行,将正常关闭。 收到信号量:USR2 钩子函数执行完毕,这里可以关闭资源。
从上面的消息我们知道,JVM成功接收到了USR2信号量,并成功执行了钩子函数。完毕!
提示:其实USR2是Linux中的第12个信号量,是为用户保留的信号量。我们可以通过这个信号量做一些定制化的操作,从而实现更复杂的功能。