JVM的关闭钩子

Stella981
• 阅读 365

什么是关闭钩子(Shutdown Hook)?先看看JavaDoc的说明:

JVM的关闭钩子

关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。这些钩子可以用于实现服务或者应用程序的清理工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源。

JVM既可以正常关闭,也可以强行关闭。正常关闭的触发方式有多种,包括:当最后一个“正常(非守护)”线程结束时,或者当调用了System.exit时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或者键入Ctrl-C)。

在正常关闭中,JVM首先调用所有已注册的关闭钩子JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,如果有(守护或者非守护)线程仍然在执行,那么这些线程接下来将与关闭进程并发执行。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true【通过Runtime.runFinalizersOnExit(true)设置】,那么JVM将运行这些Finalizer(对象重写的finalize方法),然后再停止。JVM不会停止或中断任何在关闭时仍然运行的应用程序线程。当JVM最终结束时,这些线程将被强行结束。如果关闭钩子或者Finalizer没有执行完成,那么正常关闭进程“挂起”并且JVM必须被强行关闭。当JVM被强行关闭时,只是关闭JVM,并不会运行关闭钩子

在编写关闭钩子时,需要注意以下几点:

  • 关闭钩子应该是线程安全的:它们在访问共享数据时,必须使用同步机制,并且小心地避免发生死锁,这与其他并发代码的要求相同。
  • 关闭钩子不应该对应用程序的状态(例如,其他服务是否已经关闭,或者所有的正常线程是否已经执行完成)或者JVM的关闭原因做出任何假设,因此在编写关闭钩子的代码时,必须考虑周全。
  • 关闭钩子必须尽快退出,因为 他们会延迟JVM的结束时间,而用户可能希望JVM尽快 终止。

下面是一个简单的示例:

public class T {
    @SuppressWarnings("deprecation")
    public static void main(String[] args) throws Exception {
        //启用退出JVM时执行Finalizer
        Runtime.runFinalizersOnExit(true);
        MyHook hook1 = new MyHook("Hook1");
        MyHook hook2 = new MyHook("Hook2");
        MyHook hook3 = new MyHook("Hook3");
        
        //注册关闭钩子
        Runtime.getRuntime().addShutdownHook(hook1);
        Runtime.getRuntime().addShutdownHook(hook2);
        Runtime.getRuntime().addShutdownHook(hook3);
        
        //移除关闭钩子
        Runtime.getRuntime().removeShutdownHook(hook3);
        
        //Main线程将在执行这句之后退出
        System.out.println("Main Thread Ends.");
    }
}

class MyHook extends Thread {
    private String name;
    public MyHook (String name) {
        this.name = name;
        setName(name);
    }
    public void run() {
        System.out.println(name + " Ends.");
    }
    //重写Finalizer,将在关闭钩子后调用
    protected void finalize() throws Throwable {
        System.out.println(name + " Finalize.");
    }
}

和(可能的)执行结果(因为JVM不保证关闭钩子的调用顺序,因此结果中的第二、三行可能出现相反的顺序):

Main Thread Ends.
Hook2 Ends.
Hook1 Ends.
Hook3 Finalize.
Hook2 Finalize.
Hook1 Finalize.

可以看到,main函数执行完成,首先输出的是Main Thread Ends,接下来执行关闭钩子,输出Hook2 Ends和Hook1 Ends。这两行也可以证实:JVM确实不是以注册的顺序来调用关闭钩子的。而由于hook3在调用了addShutdownHook后,接着对其调用了removeShutdownHook将其移除,于是hook3在JVM退出时没有执行,因此没有输出Hook3 Ends。

另外,由于MyHook类实现了finalize方法,而main函数中第一行又通过Runtime.runFinalizersOnExit(true)打开了退出JVM时执行Finalizer的开关,于是3个hook对象的finalize方法被调用,输出了3行Finalize。

注意,多次调用addShutdownHook来注册同一个关闭钩子将会抛出IllegalArgumentException:

Exception in thread "main" java.lang.IllegalArgumentException: Hook previously registered
    at java.lang.ApplicationShutdownHooks.add(ApplicationShutdownHooks.java:72)
    at java.lang.Runtime.addShutdownHook(Runtime.java:211)
    at T.main(T.java:12)

另外,从JavaDoc中得知:

Once the shutdown sequence has begun it can be stopped only by invoking the halt method, which forcibly terminates the virtual machine.

Once the shutdown sequence has begun it is impossible to register a new shutdown hook or de-register a previously-registered hook. Attempting either of these operations will cause an IllegalStateException to be thrown.

“一旦JVM关闭流程开始,就只能通过调用halt方法来停止该流程,也不可能再注册或移除关闭钩子了,这些操作将导致抛出IllegalStateException”。

如果在关闭钩子中关闭应用程序的公共的组件,如日志服务,或者数据库连接等,像下面这样:

Runtime.getRuntime().addShutdownHook(new Thread() {
    public void run() {
        try { 
            LogService.this.stop();
        } catch (InterruptedException ignored){
            //ignored
        }
    }
});

由于关闭钩子将并发执行,因此在关闭日志时可能导致其他需要日志服务的关闭钩子产生问题。为了避免这种情况,可以使关闭钩子不依赖那些可能被应用程序或其他关闭钩子关闭的服务。实现这种功能的一种方式是对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之前出现竞态条件或死锁等问题。

点赞
收藏
评论区
推荐文章
秃头王路飞 秃头王路飞
4个月前
webpack5手撸vue2脚手架
webpack5手撸vue相信工作个12年的小伙伴们在面试的时候多多少少怕被问到关于webpack方面的知识,本菜鸟最近闲来无事,就尝试了手撸了下vue2的脚手架,第一次发帖实在是没有经验,望海涵。languageJavaScript"name":"vuecliversion2","version":"1.0.0","desc
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
小森森 小森森
4个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
Stella981 Stella981
1年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
1年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue
helloworld_34035044 helloworld_34035044
6个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为