Java程序经常也会遇到进程挂掉的情况,一些状态没有正确的保存下来,这时候就需要在JVM关掉的时候执行一些清理现场的代码。
JAVA中的ShutdownHook提供了比较好的方案。
JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在一下几种场景中被调用:
1. 程序正常退出
2. 使用System.exit()
3. 终端使用Ctrl+C触发的中断
4. 系统关闭
5. OutOfMemory宕机
6. 使用Kill pid命令干掉进程(注:在使用kill -9 pid时,是不会被调用的)
下面是JDK1.7中关于钩子的定义:
public void addShutdownHook(Thread hook)
参数:
hook - An initialized but unstarted Thread object
抛出:
IllegalArgumentException - If the specified hook has already been registered, or if it can be determined that the hook is already running or has already been run
IllegalStateException - If the virtual machine is already in the process of shutting down
SecurityException - If a security manager is present and it denies RuntimePermission("shutdownHooks")
从以下版本开始:
1.3
另请参见:
removeShutdownHook(java.lang.Thread), halt(int), exit(int)
首先来测试第一种,程序正常退出的情况:
package com.hook;
import java.util.concurrent.TimeUnit;
public class HookTest
{
public void start()
{
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run()
{
System.out.println("Execute Hook.....");
}
}));
}
public static void main(String[] args)
{
new HookTest().start();
System.out.println("The Application is doing something");
try
{
TimeUnit.MILLISECONDS.sleep(5000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
运行结果:
The Application is doing something
Execute Hook.....
如上可以看到,当main线程运行结束之后就会调用关闭钩子。
下面再来测试第五种情况(顺序有点乱,表在意这些细节):
package com.hook;
import java.util.concurrent.TimeUnit;
public class HookTest2
{
public void start()
{
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run()
{
System.out.println("Execute Hook.....");
}
}));
}
public static void main(String[] args)
{
new HookTest().start();
System.out.println("The Application is doing something");
byte[] b = new byte[500*1024*1024];
try
{
TimeUnit.MILLISECONDS.sleep(5000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
运行参数设置为:-Xmx20M 这样可以保证会有OutOfMemoryError的发生。
运行结果:
The Application is doing something
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.hook.HookTest2.main(HookTest2.java:22)
Execute Hook.....
可以看到程序遇到内存溢出错误后调用关闭钩子,与第一种情况中,程序等待5000ms运行结束之后推出调用关闭钩子不同。
接下来再来测试第三种情况:
package com.hook;
import java.util.concurrent.TimeUnit;
public class HookTest3
{
public void start()
{
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run()
{
System.out.println("Execute Hook.....");
}
}));
}
public static void main(String[] args)
{
new HookTest3().start();
Thread thread = new Thread(new Runnable(){
@Override
public void run()
{
while(true)
{
System.out.println("thread is running....");
try
{
TimeUnit.MILLISECONDS.sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
});
thread.start();
}
}
在命令行中编译:javac com/hook/HookTest3.java
在命令行中运行:Java com.hook.HookTest3 (之后按下Ctrl+C)
ShutdownHookManager的创建是在SparkContext中,为了在Spark程序挂掉的时候,处理一些清理工作。
/** ShutdownHookManager的创建,为了在Spark程序挂掉的时候,处理一些清理工作 */
_shutdownHookRef = ShutdownHookManager.addShutdownHook(
ShutdownHookManager.SPARK_CONTEXT_SHUTDOWN_PRIORITY) { () =>
logInfo("Invoking stop() from shutdown hook")
// 这调用停止方法。关闭SparkContext,我就搞不懂了
stop()
}
来看看整体代码
package org.apache.spark.util
import java.io.File
import java.util.PriorityQueue
import scala.util.Try
import org.apache.hadoop.fs.FileSystem
import org.apache.spark.internal.Logging
/**
* Various utility methods used by Spark.
*
* Spark使用的各种实用方法。
*/
private[spark] object ShutdownHookManager extends Logging {
val DEFAULT_SHUTDOWN_PRIORITY = 100 // 默认的ShutdownHookManager优先级
/**
* The shutdown priority of the SparkContext instance. This is lower than the default
* priority, so that by default hooks are run before the context is shut down.
*
* SparkContext实例的shutdown优先级。这比默认的优先级要低,因此在默认情况下,在关闭上下文之前运行默认的hooks。
*/
val SPARK_CONTEXT_SHUTDOWN_PRIORITY = 50
/**
* The shutdown priority of temp directory must be lower than the SparkContext shutdown
* priority. Otherwise cleaning the temp directories while Spark jobs are running can
* throw undesirable errors at the time of shutdown.
*
* temp目录的关闭优先级必须低于SparkContext关闭的优先级。否则,当Spark作业正在运行时,清理temp目录将会在关闭时抛出错误的错误。
*/
val TEMP_DIR_SHUTDOWN_PRIORITY = 25
// 懒加载
private lazy val shutdownHooks = {
val manager = new SparkShutdownHookManager()
// 运行所有的hook,并且添加进去
manager.install()
manager
}
private val shutdownDeletePaths = new scala.collection.mutable.HashSet[String]()
// Add a shutdown hook to delete the temp dirs when the JVM exits
// 当JVM退出时,添加一个关闭钩子来删除temp dirs
logDebug("Adding shutdown hook") // force eager creation of logger
addShutdownHook(TEMP_DIR_SHUTDOWN_PRIORITY) { () =>
logInfo("Shutdown hook called")
// we need to materialize the paths to delete because deleteRecursively removes items from
// shutdownDeletePaths as we are traversing through it.
shutdownDeletePaths.toArray.foreach { dirPath =>
try {
logInfo("Deleting directory " + dirPath)
// 递归地删除文件或目录及其内容。 如果删除失败,则抛出异常。
Utils.deleteRecursively(new File(dirPath))
} catch {
case e: Exception => logError(s"Exception while deleting Spark temp dir: $dirPath", e)
}
}
}
// Register the path to be deleted via shutdown hook
// 通过关闭hook注册要删除的路径
def registerShutdownDeleteDir(file: File) {
// 得到文件的绝对路径
val absolutePath = file.getAbsolutePath()
// 假如到要删除文件路径的集合
shutdownDeletePaths.synchronized {
shutdownDeletePaths += absolutePath
}
}
// Remove the path to be deleted via shutdown hook 删除通过关闭hook删除的路径
def removeShutdownDeleteDir(file: File) {
val absolutePath = file.getAbsolutePath()
// 删除文件
shutdownDeletePaths.synchronized {
shutdownDeletePaths.remove(absolutePath)
}
}
// Is the path already registered to be deleted via a shutdown hook ?
// 已经注册的路径是否通过关闭hook被删除?
// 判断shutdownDeletePaths中是否包含给定的路径,如果包含返回true,否则返回false
def hasShutdownDeleteDir(file: File): Boolean = {
val absolutePath = file.getAbsolutePath()
shutdownDeletePaths.synchronized {
shutdownDeletePaths.contains(absolutePath)
}
}
// Note: if file is child of some registered path, while not equal to it, then return true;
// else false. This is to ensure that two shutdown hooks do not try to delete each others
// paths - resulting in IOException and incomplete cleanup.
// 注意:如果文件是某个已注册路径的子元素,而不等于它,则返回true;其他错误的。
// 这是为了确保两个关闭hooks不会试图删除彼此的路径——导致IOException和不完整的清理。
def hasRootAsShutdownDeleteDir(file: File): Boolean = {
val absolutePath = file.getAbsolutePath()
val retval = shutdownDeletePaths.synchronized {
shutdownDeletePaths.exists { path =>
!absolutePath.equals(path) && absolutePath.startsWith(path)
}
}
if (retval) {
logInfo("path = " + file + ", already present as root for deletion.")
}
retval
}
/**
* Detect whether this thread might be executing a shutdown hook. Will always return true if
* the current thread is a running a shutdown hook but may spuriously return true otherwise (e.g.
* if System.exit was just called by a concurrent thread).
*
* 检测此线程是否正在执行关闭hook。如果当前线程是一个正在运行的关闭hook,但可能会错误地返回true(例如,如果系统),
* 则将始终返回true。退出是由一个并发线程调用的。
*
* Currently, this detects whether the JVM is shutting down by Runtime#addShutdownHook throwing
* an IllegalStateException.
*
* 当前,这检测到JVM是否在Runtime#addShutdownHook,抛出了一个IllegalStateException异常。
*/
def inShutdown(): Boolean = {
try {
val hook = new Thread {
override def run() {}
}
// 这一点先加入后移除 是什么意思啊?
// scalastyle:off runtimeaddshutdownhook
Runtime.getRuntime.addShutdownHook(hook)
// scalastyle:on runtimeaddshutdownhook
Runtime.getRuntime.removeShutdownHook(hook)
} catch {
case ise: IllegalStateException => return true
}
false
}
/**
* Adds a shutdown hook with default priority. 添加默认优先级的 shutdown hook。
*
* @param hook The code to run during shutdown.
* @return A handle that can be used to unregister the shutdown hook.
*/
def addShutdownHook(hook: () => Unit): AnyRef = {
addShutdownHook(DEFAULT_SHUTDOWN_PRIORITY)(hook)
}
/**
* Adds a shutdown hook with the given priority. Hooks with lower priority values run
* first.
*
* 根据一个指定的优先级添加一个shutdown hook,优先级低的Hooks优先被运行
*
* @param hook The code to run during shutdown.
* @return A handle that can be used to unregister the shutdown hook.
*/
def addShutdownHook(priority: Int)(hook: () => Unit): AnyRef = {
shutdownHooks.add(priority, hook)
}
/**
* Remove a previously installed shutdown hook. 删除先前安装的shutdown hook
*
* @param ref A handle returned by `addShutdownHook`.
* @return Whether the hook was removed.
*/
def removeShutdownHook(ref: AnyRef): Boolean = {
shutdownHooks.remove(ref)
}
}
private [util] class SparkShutdownHookManager {
// 权限队列
private val hooks = new PriorityQueue[SparkShutdownHook]()
@volatile private var shuttingDown = false
/**
* Install a hook to run at shutdown and run all registered hooks in order.
* 安装一个hook来运行关闭,并运行所有已注册的hooks。
*/
def install(): Unit = {
val hookTask = new Runnable() {
override def run(): Unit = runAll()
}
org.apache.hadoop.util.ShutdownHookManager.get().addShutdownHook(
hookTask, FileSystem.SHUTDOWN_HOOK_PRIORITY + 30)
}
def runAll(): Unit = {
shuttingDown = true
var nextHook: SparkShutdownHook = null
while ({ nextHook = hooks.synchronized { hooks.poll() }; nextHook != null }) {
Try(Utils.logUncaughtExceptions(nextHook.run()))
}
}
def add(priority: Int, hook: () => Unit): AnyRef = {
hooks.synchronized {
if (shuttingDown) {
throw new IllegalStateException("Shutdown hooks cannot be modified during shutdown.")
}
val hookRef = new SparkShutdownHook(priority, hook)
hooks.add(hookRef)
hookRef
}
}
def remove(ref: AnyRef): Boolean = {
hooks.synchronized { hooks.remove(ref) }
}
}
private class SparkShutdownHook(private val priority: Int, hook: () => Unit)
extends Comparable[SparkShutdownHook] {
override def compareTo(other: SparkShutdownHook): Int = {
other.priority - priority
}
def run(): Unit = hook()
}