如何优雅地关闭JVM?看看钩子函数如何一步实现

简介: 程序的启动很简单,启动的时候通常会做一些预加载资源的操作。但是有时候关闭的时候,启动的时候预加载的资源并没有完全清理干净,因此可以使用钩子函数来完成。

前言


1、基本概述


程序的启动很简单,启动的时候通常会做一些预加载资源的操作。但是有时候关闭的时候,启动的时候预加载的资源并没有完全清理干净,因此可以使用钩子函数来完成。


2、JVM关闭的场景分类


直接看一张图吧,本图来自博客园的BarryWang,特在此说明。

v2-78946ec3c49880217596b7ca9359e074_1440w.jpg

从上面可以看到,JVM关闭主要分为了三类,第一种是正常的关闭,第二种是异常关闭的情况,第三种是强制关闭的情况。对于前两种方式我们可以使用钩子函数优雅的关闭,但是强制关闭的时候钩子函数并不起作用。


有了这些概念,我们直接使用一个案例进行演示,再进行分析。


一、代码演示钩子函数


1、JVM正常关闭


直接看代码吧,

public class Test {
 public void start(){
  Runtime.getRuntime().addShutdownHook(new Thread(()-> 
    System.out.println("钩子函数被执行,可以在这里关闭资源")
  ));
 }
 public static void main(String[] args) throws Exception{
  new Test().start();
  System.out.println("主应用程序在执行");
 }
}
//控制台输出
//主应用程序在执行
//钩子函数被执行,可以在这里关闭资源

看控制台打印,可以发现,主应用程序执行完之后就会调用钩子函数,接下来就会正式的关闭JVM。


2、异常关闭


还是直接看代码演示,这里我们演示异常关闭的第二种OOM的情况,我们可以先设置堆的大小为20M,然后在代码中创建一个500M的对象,这样就会OOM。参数是-Xmx20M


public class Test {
 public void start(){
  Runtime.getRuntime().addShutdownHook(new Thread(()-> 
    System.out.println("钩子函数被执行,可以在这里关闭资源")
  ));
 }
 public static void main(String[] args) throws Exception{
  new Test().start();
  System.out.println("主应用程序在执行");
  Runtime.getRuntime().halt(1);
  byte[] b = new byte[500*1024*1024];
 }
}
//控制台输出
//主应用程序在执行
//钩子函数被执行,可以在这里关闭资源

从控制台可以看出,钩子函数在异常关闭的时候依然会被调用。


3、强制关闭


这里我们使用Runtime.getRuntime().halt()来演示强势关闭。这个方法和System.exit的区别是,System.exit会执行钩子函数,但是Runtime.getRuntime().halt()不会。

public class Test {
 public void start(){
  Runtime.getRuntime().addShutdownHook(new Thread(()-> 
    System.out.println("钩子函数被执行,可以在这里关闭资源")
  ));
 }
 public static void main(String[] args) throws Exception{
  new Test().start();
  System.out.println("主应用程序在执行");
  Runtime.getRuntime().halt(1);
 }
}
//控制台输出
//主应用程序在执行

从上面代码的输出可以看出,调用了Runtime.getRuntime().halt(1)就会强制关闭JVM,钩子函数来不及执行就关闭了。而使用System.exit依然会执行。所以一般使用System.exit来关闭JVM。


4、移除钩子函数


上面演示了钩子函数的作用,有时候我们想移除也比较简单。

public class Test {
 public static void main(String[] args) throws Exception{
  //new Test().start();
  Thread willNotRun = new Thread(() -> 
   System.out.println("Won't run!"));
  Runtime.getRuntime().addShutdownHook(willNotRun);
  System.out.println("主应用程序在执行");
  Runtime.getRuntime().removeShutdownHook(willNotRun);
 }
}
//控制台输出
//主应用程序在执行

OK,钩子函数的基本操作就写到这,使用起来比较简单,不过我之前看过Spring的启动流程,所以又去那个启动流程看了一波,发现也使用到了钩子函数。


二、典型应用场景


1、Spring使用


Spring在关闭上下文的时候,可以使用钩子函数来关闭残留的资源。方法是使用ApplicationContext注册一个钩子函数即可。

ApplicationContext.registerShutdownHook();
//上面的这句代码可以分析进去看看
public void registerShutdownHook() {
    if (this.shutdownHook == null) {
      this.shutdownHook = new Thread() {
        @Override
        public void run() {
          //Spring正常关闭
          doClose();
        }
      };
      //调用钩子函数关闭残留资源
      Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
}

从源码可以看出,Spring其实也是调用了Java的钩子函数进行关闭的。


2、其他使用


我在很多博客中也看到了spark和hadoop的关闭,由于我没看过源码,所以这里我说一下结论,对于其他的使用场景,基本上也是调用了Java的钩子函数来执行的。


结论


在关闭JVM的时候,我们可以封装钩子函数去优雅的关闭线程。不过在使用的时候还需要注意以下几个方面:


1、钩子函数本质是个线程


多个钩子会并发执行,JVM并不保证它们的执行顺序;因此最好是在一个钩子中执行一系列操作。


2、钩子中不能再新建钩子


在关闭钩子中,不能执行注册、移除钩子的操作,否则JVM抛出 IllegalStateException。也不能使用System.exit(),前面提到System.exit()会触发钩子函数的执行,但是Runtime.halt()可以,因为Runtime.halt()可以强制关闭。


3、钩子里最好不要有耗时操作


钩子函数主要用于关闭残留资源,因此不要有一些耗时的操作。

OK,先写到这。

相关文章
|
Java Linux iOS开发
JNI用C加载JDK产生JVM虚拟机,并运行JAVA类main函数(MACOS/LINUX/WINDOWS)
JNI用C加载JDK产生JVM虚拟机,并运行JAVA类main函数(MACOS/LINUX/WINDOWS)
137 0
|
Java 数据库连接 Spring
Java基础内容之JVM钩子hooks函数
请问在Spring中,如果JVM异常终止,Spring是如何保证会释放掉占用的资源,比如说数据库连接等资源呢? 钩子函数非常简单,简单到小编只用摘抄一段Spring代码即可。走你,现在开始。
331 0
|
1月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
1月前
|
存储 算法 Oracle
不好意思!耽误你的十分钟,JVM内存布局还给你
先赞后看,南哥助你Java进阶一大半在2006年加州旧金山的JavaOne大会上,一个由顶级Java开发者组成的周年性研讨会,公司突然宣布将开放Java的源代码。于是,下一年顶级项目OpenJDK诞生。Java生态发展被打开了新的大门,Java 7的G1垃圾回收器、Java 8的Lambda表达式和流API…大家好,我是南哥。一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
不好意思!耽误你的十分钟,JVM内存布局还给你
|
1月前
|
存储 算法 Java
JVM自动内存管理之垃圾收集算法
文章概述了JVM内存管理和垃圾收集的基本概念,提供一个关于JVM内存管理和垃圾收集的基础理解框架。
JVM自动内存管理之垃圾收集算法
|
1月前
|
存储 Java 程序员
JVM自动内存管理之运行时内存区
这篇文章详细解释了JVM运行时数据区的各个组成部分及其作用,有助于理解Java程序运行时的内存布局和管理机制。
JVM自动内存管理之运行时内存区
|
1月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
1月前
|
存储 安全 Java
JVM内存结构
这篇文章详细介绍了Java虚拟机(JVM)的内存结构,包括类的加载过程、类加载器的双亲委派机制、沙箱安全机制、程序计数器、Java栈、Java堆、本地方法和本地方法栈等关键组件及其作用。
JVM内存结构
|
2月前
|
运维 Java Linux
(九)JVM成神路之性能调优、GC调试、各内存区、Linux参数大全及实用小技巧
本章节主要用于补齐之前GC篇章以及JVM运行时数据区的一些JVM参数,更多的作用也可以看作是JVM的参数列表大全。对于开发者而言,能够控制JVM的部分也就只有启动参数了,同时,对于JVM的性能调优而言,JVM的参数也是基础。
|
2月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。