88、Java运行时系统的关闭、功能扩展与安全机制解析

Java运行时系统的关闭、功能扩展与安全机制解析

1. 运行时系统关闭机制

1.1 正常关闭与显式关闭

通常,虚拟机的执行会在最后一个用户线程终止时结束。此外,也可以使用 Runtime 类的 exit 方法显式关闭,该方法需要传入一个整数状态码,用于向执行虚拟机的环境传达任务完成情况,0 表示任务成功完成,非 0 表示失败。调用 exit 方法会立即终止运行时系统中的所有线程,这些线程不会被中断或停止,而是随着虚拟机停止运行而直接消失,并且不会执行 finally 子句。

除了内部调用 exit 或最后一个用户线程终止触发关闭外,虚拟机还可以从外部关闭,例如用户通过键盘中断(在许多系统中通过键入 Ctrl + C )、用户注销或计算机关机等操作。

所有与关闭运行时系统相关的方法都会进行安全检查,如果没有所需的权限,将抛出 SecurityException

1.2 关闭钩子

应用程序可以向运行时系统注册关闭钩子(Shutdown Hooks)。关闭钩子是线程,代表在虚拟机退出前应执行的操作,通常用于清理外部资源,如文件和网络连接。

以下是与关闭钩子相关的方法:
- public void addShutdownHook(Thread hook) :注册一个新的虚拟机关闭钩子。如果钩子已经注册或已经启动,将抛出 IllegalArgumentException
- public boolean removeShutdownHook(Thread hook) :注销先前注册的虚拟机关闭钩子。如果钩子已注册并成功注销,返回 true ;如果钩子未注册,返回 false

需要注意的是,在关闭过程开始后,不能添加或删除关闭钩子,否则会抛出 IllegalStateException 。当关闭启动时,虚拟机会调用所有关闭钩子线程的 start 方法,但无法保证这些线程的执行顺序,它们可能在其他关闭钩子线程之前、之后或同时执行,具体取决于线程调度。

例如,假设编写一个将状态存储在数据库中的类,可能会注册一个关闭钩子来关闭数据库。但使用该类的程序可能会注册自己的关闭钩子,用于通过该类将一些最终状态信息写入数据库。如果先运行关闭数据库的钩子,程序的关闭钩子在写入最终状态时将失败。可以通过在需要时重新打开数据库来改善这种情况,但这可能会增加类的复杂性,并可能引入一些不良的竞争条件。或者设计类时使其不需要任何关闭钩子。

关闭钩子线程会与系统中的其他线程并发执行。如果关闭是由于最后一个用户线程终止而启动的,关闭钩子线程将与系统中的任何守护线程并发执行;如果关闭是通过调用 exit 启动的,关闭钩子线程将与守护线程和用户线程并发执行。因此,编写关闭钩子线程时必须小心,确保正确同步,避免潜在的死锁。

关闭钩子应该快速执行。当用户中断程序时,他们期望程序能快速终止;当虚拟机因用户注销或计算机关机而终止时,虚拟机可能只有很短的时间来完成关闭操作。与用户的交互应该在关闭之前进行,而不是在关闭过程中。

1.3 关闭序列

当最后一个用户线程终止、调用 Runtime.exit 方法或外部环境向虚拟机发出关闭信号时,关闭序列将启动。启动关闭时,所有关闭钩子线程将被启动并允许运行至完成。如果任何一个关闭钩子线程未能终止,关闭序列将无法完成。如果关闭是从内部启动的,虚拟机将不会终止;如果关闭信号来自外部环境,关闭失败可能导致虚拟机被强制终止。

如果关闭钩子线程引发未捕获的异常,不会采取特殊操作,它将像线程中任何其他未捕获的异常一样处理,不会导致关闭过程中止。

当最后一个关闭钩子线程终止后,将调用 halt 方法,该方法会使虚拟机停止运行。 halt 方法也接受一个整数状态作为参数,其含义与 exit 相同,0 表示整个虚拟机成功执行。关闭钩子可以调用 halt 来结束关闭阶段,但直接调用 halt (无论是在关闭阶段之前还是期间)是危险的,因为它会阻止未完成的钩子进行清理操作,通常不建议这样做。

如果在关闭过程中调用 exit ,该调用将无限期阻塞,其对关闭序列的影响未明确规定,因此应避免这样做。

在极少数情况下,虚拟机可能会中止而不是进行有序关闭,例如检测到虚拟机内部错误(如错误的本地代码覆盖系统数据结构),或者托管虚拟机的环境强制虚拟机中止(如在 UNIX 系统中, SIGKILL 信号将强制虚拟机中止)。当虚拟机以这种方式被强制终止时,无法保证关闭钩子是否会运行。

1.4 关闭策略

一般来说,应该让程序正常完成,而不是使用 exit 强制关闭,特别是对于多线程程序,决定程序退出的线程可能不清楚其他线程的运行情况。

设计多线程应用程序时,使其在程序的任意点安全退出要么很简单,要么极其复杂。如果没有线程执行必须完成的操作,例如有一千个线程都在尝试解决一个数值问题,那么设计起来很简单;但如果有任何线程执行必须完成的操作,设计就会很复杂。

可以通过编写线程来响应 interrupt 方法,向所有线程广播关闭请求,即调用应用程序父 ThreadGroup interrupt 方法。但这不能保证程序会终止,因为使用的库可能创建了不响应中断请求的用户线程,例如 AWT 图形库。

有两种情况必须调用 exit :一是这是终止某些线程从而终止应用程序的唯一方法;二是应用程序必须返回状态码。在这两种情况下,需要延迟调用 exit ,直到所有应用程序线程有机会干净地终止。一种方法是让发起终止的线程加入其他线程,以了解这些线程何时终止。但应用程序可能需要维护自己创建的线程列表,因为简单检查 ThreadGroup 可能会返回不会终止的库线程,调用 join 方法不会返回。

关闭程序的决策应该在应用程序的高层做出,通常在 main 方法、顶级应用程序 ThreadGroup 中线程的 run 方法或图形用户界面中响应事件的代码中。遇到错误的方法应通过异常报告这些错误,以便高层代码采取适当的操作。实用代码不应因遇到错误而终止应用程序,因为它缺乏做出明智决策所需的应用程序知识,因此使用异常与高层代码进行通信。

2. 运行时类的其他功能

2.1 加载本地代码

在 Java 中, native 修饰符可应用于方法,表示该方法的实现由本地代码提供。在运行时,必须将这些方法的本地代码加载到虚拟机中,以便执行这些方法。这个过程的细节因系统而异,但 Runtime 类提供了两个方法来实现这一功能:
- public void loadLibrary(String libname) :加载具有指定库名的动态库。该库对应于本地文件系统中的一个文件,系统知道在某些位置查找库文件,库名到文件名的实际映射因系统而异。
- public void load(String filename) :将 filename 指定的文件作为动态库加载。与 loadLibrary 不同, load 允许库文件位于文件系统的任何位置。

通常,具有本地方法的类会在类的初始化过程中,通过将 load 调用放在静态初始化块中来加载相应的库。不过,也可以在实际调用方法时进行延迟加载。

加载本地代码库是一项特权操作,如果没有所需的权限,将抛出 SecurityException 。如果找不到库或,系统在尝试加载库时发生错误,将抛出 UnsatisfiedLinkError

System 类中的 mapLibraryName 方法可以将库名映射为系统相关的库名。例如,库名 “awt” 在 Windows 下可能映射为 “awt.dll”,在 UNIX 下可能映射为 “libawt.so”。

2.2 调试

Runtime 类提供了两个方法来支持应用程序的调试:
- public void traceInstructions(boolean on) :根据 on 的值启用或禁用指令跟踪。如果 on true ,该方法建议虚拟机在执行每条指令时发出调试信息。
- public void traceMethodCalls(boolean on) :根据 on 的值启用或禁用方法调用跟踪。如果 on true ,该方法建议虚拟机在调用每个方法时发出调试信息。

调试信息的格式以及输出到的文件或其他输出流取决于主机环境。每个虚拟机可以自由处理这些调用,包括在本地运行时系统没有地方放置跟踪输出时忽略它们,但在开发环境中这些方法可能会起作用。

这些方法是相对底层的调试工具,通常,虚拟机还会通过 JVM™ 工具接口(JVMTI)支持高级调试器和分析器,感兴趣的读者可以查阅相关规范以获取更多信息。

3. 安全机制

3.1 安全管理器类

java.lang.SecurityManager 类允许应用程序实现安全策略,通过在执行可能不安全或敏感的操作之前,确定操作是否在允许执行的安全上下文中进行,从而允许或禁止该操作。

SecurityManager 类包含许多以 “check” 开头的方法,标准库中的各种方法在执行某些潜在敏感操作(如访问文件、创建和控制线程、创建类加载器、执行某些形式的反射以及控制安全本身)之前会调用这些方法。调用检查方法的典型代码如下:

SecurityManager security = System.getSecurityManager();
if (security != null) {
    security.checkXXX(...);
}

安全管理器有机会通过抛出异常来阻止操作完成。如果操作被允许,安全管理器例程将直接返回;如果操作不被允许,将抛出 SecurityException

可以使用 System 类的方法获取和设置安全管理器:
- public static void setSecurityManager(SecurityManager s) :设置系统安全管理器对象。如果已经存在安全管理器,新的管理器将替换它,但前提是现有管理器支持替换并且有替换权限,否则将抛出 SecurityException
- public static SecurityManager getSecurityManager() :获取系统安全管理器。如果未设置,则返回 null ,并假定拥有所有权限。

安全管理器将实际的安全检查委托给访问控制对象。每个检查方法只是调用安全管理器的 checkPermission 方法,并传递请求操作的相应 java.security.Permission 对象。 checkPermission 的默认实现会调用 java.security.AccessController.checkPermission(perm)

如果允许请求的访问, checkPermission 将安静地返回;如果被拒绝,将抛出 java.security.AccessControlException (一种 SecurityException )。

这种形式的 checkPermission 总是在当前线程的上下文中执行安全检查,即线程所属的一组保护域。由于保护域是具有相同权限的一组类,当调用栈包含不同类对象上的活动方法时,线程可以是多个保护域的成员。

安全管理器的 getSecurityContext 方法将线程的安全上下文作为 java.security.AccessControlContext 对象返回。该类也定义了一个 checkPermission 方法,但它在封装的上下文中评估该方法,而不是在调用线程的上下文中。当一个上下文(如线程)必须为另一个上下文执行安全检查时,会使用此功能。

例如,考虑一个执行来自不同源的工作请求的工作线程。工作线程有权执行一系列操作,但工作请求的提交者可能没有这些相同的权限。当提交工作请求时,提交者的安全上下文会与工作请求一起存储。当工作线程执行工作时,它使用存储的上下文来确保所有安全检查都是针对请求提交者而不是工作线程本身进行的。在简单情况下,可能涉及调用安全管理器的双参数 checkPermission 方法,该方法接受 Permission 对象和 AccessControlContext 对象作为参数。在更一般的情况下,工作线程可能会使用 AccessController doPrivileged 方法。

3.2 权限

权限分为多个类别,每个类别由特定的类管理,例如:
| 权限类别 | 对应类 |
| ---- | ---- |
| 文件 | java.io.FilePermission |
| 网络 | java.net.NetPermission |
| 属性 | java.util.PropertyPermission |
| 反射 | java.lang.reflect.ReflectPermission |
| 运行时 | java.lang.RuntimePermission |
| 安全 | java.security.SecurityPermission |
| 序列化 | java.io.SerializablePermission |
| 套接字 | java.net.SocketPermission |

除了 FilePermission SocketPermission 外,其他权限类都是 java.security.BasicPermission 的子类,而 BasicPermission 本身是权限顶级类 java.security.Permission 的抽象子类。 BasicPermission 基于名称定义简单的权限。例如,名称为 “exitVM” 的 RuntimePermission 表示调用 Runtime.exit 关闭虚拟机的权限。以下是其他一些基本 RuntimePermission 名称及其代表的含义:
- “createClassLoader”:调用 ClassLoader 构造函数。
- “setSecurityManager”:调用 System.setSecurityManager
- “modifyThread”:调用 Thread 方法 interrupt setPriority setDaemon setName

基本权限要么拥有,要么没有。基本权限的名称遵循系统属性使用的分层命名方案,名称末尾可以跟一个 “.” 后接 “ ” 或单独的 “ ” 表示通配符匹配。例如,”java. ” 或 “ ” 是有效的,而 “ java” 或 “x y” 是无效的。

FilePermission SocketPermission Permission 的子类,这些类的名称语法比基本权限更复杂。例如,对于 FilePermission 对象,权限名称是文件(或目录)的路径名,可以使用 “*” 表示指定目录中的所有文件,使用 “-” 表示指定目录中的所有文件以及所有子目录中的所有文件。

所有权限都可以关联一个操作列表,用于定义该对象允许的不同操作。例如, FilePermission 对象的操作列表可以包含 “read”、”write”、”execute” 或 “delete” 的任意组合,指定可以对命名文件(或目录)执行的操作。许多基本权限不使用操作列表,但有些权限(如 PropertyPermission )会使用。 PropertyPermission 的名称是它代表的属性的名称,操作可以是 “read” 或 “write”,分别允许使用该属性名称调用 System.getProperty System.setProperty 。例如,名称为 “java.*” 且操作是 “read” 的 PropertyPermission 允许检索所有以 “java.” 开头的系统属性的值。

3.3 安全策略

给定运行时系统执行的安全策略由 java.security.Policy 对象表示,更具体地说,由抽象 Policy 类的具体子类表示。 Policy 对象根据代码源维护分配给不同保护域的权限集。安全策略如何传达给 Policy 对象取决于该策略的实际实现,默认实现是使用策略文件列出授予每个代码源的不同权限。

例如,以下是一个示例策略文件条目,授予来自 /home/sysadmin 目录的代码对 /tmp/abc 文件的读取访问权限:

grant codeBase "file:/home/sysadmin/" {
    permission java.io.FilePermission "/tmp/abc", "read";
};

要了解本地系统中如何定义安全策略,请查阅本地文档。

由引导加载器加载的类被认为是受信任的,不需要在安全策略中显式设置权限。一些虚拟机实现还支持标准扩展机制,通过将类放置在扩展类加载器访问的特殊位置,可以将这些类标识为受信任的,同样不需要显式设置权限。

3.4 访问控制器和特权执行

AccessController 类用于三个目的:
- 提供安全管理器用于执行安全检查的基本 checkPermission 方法。
- 通过 getContext 方法创建当前调用上下文的 “快照”,返回一个 AccessControlContext
- 提供一种以特权方式运行代码的方法,从而改变原本与代码关联的权限集。

前面已经讨论了前两个用途,这里主要介绍特权执行的含义。

保护域(由 java.security.ProtectionDomain 表示)包含代码源(由 java.security.CodeSource 表示)以及根据当前生效的安全策略授予该代码源的权限。代码源扩展了代码库(类加载的位置)的概念,包括与这些类关联的数字证书信息。数字证书可用于验证文件的真实性,并确保文件未被篡改。具有相同证书且来自同一位置的类被放置在同一域中,一个类只属于一个保护域。具有相同权限但来自不同代码源的类属于不同的域。

每个小程序或应用程序在其适当的域中运行,由其代码源决定。对于小程序(或在安全管理器下运行的应用程序)要被允许执行安全操作,小程序或应用程序必须被授予该特定操作的权限。更具体地说,每当尝试执行安全操作时,执行线程到该点所遍历的所有代码都必须具有该操作的权限,除非线程上的某些代码被标记为特权代码。

例如,假设在一个执行线程中进行访问控制检查,该线程有多个调用者链(可以看作是多个可能跨越保护域边界的方法调用)。当最近的调用者调用 AccessController.checkPermission 方法时,决定是否允许请求操作的基本算法如下:
如果调用链中任何调用者的代码没有请求的权限,将抛出 AccessControlException ,除非代码被授予该权限的调用者被标记为特权代码,并且该调用者随后直接或间接调用的所有方都具有该权限。

将代码标记为特权代码可以使一段受信任的代码临时允许执行比调用它的代码直接可用的更多操作。在某些情况下,这是必要的。例如,应用程序可能不被允许直接访问包含字体的文件,但显示文档的系统实用程序必须代表用户获取这些字体,这就要求系统实用程序代码在获取字体时成为特权代码。

AccessController doPrivileged 方法接受一个 java.security.PrivilegedAction<T> 对象作为参数,其 run 方法定义了要标记为特权的代码。例如,调用 doPrivileged 可以如下所示:

void someMethod() {
    // ...正常代码...
    AccessController.doPrivileged(
        new PrivilegedAction<Object>() {
            public Object run() {
                // 特权代码,例如:
                System.loadLibrary("awt");
                return null; // 无返回值
            }
        }
    );
    // ...正常代码...
}

doPrivileged 方法以特权模式执行 run 方法。特权执行是一种让具有给定权限的类临时将该权限授予执行特权代码的线程的方式,但不会让你获得原本没有的权限。定义 someMethod 的类必须具有 RuntimePermission("loadLibrary.awt") 权限,否则任何调用 someMethod 的线程都将抛出 SecurityException 。在 PrivilegedAction 对象的 run 方法返回后,特权将被撤销。

PrivilegedAction<T> 接口的单个方法 run 返回一个 T 对象。 doPrivileged 还有另一种形式,接受一个 PrivilegedExceptionAction<T> 对象,其 run 方法也返回 T ,但可以抛出任何已检查异常。这两种方法都有一个重载形式,接受一个 AccessControlContext 对象作为参数,并使用该上下文来确定特权代码应运行的权限。

使用 doPrivileged 时必须格外小心,确保特权代码段尽可能短,并且只执行完全可控的操作。例如,一个具有所有 I/O 权限的方法接受一个 Runnable 参数并在特权代码段中调用其 run 方法是极其危险的,除非不介意该方法删除磁盘上的所有文件。

4. 总结与实践建议

4.1 运行时系统关闭总结

  • 关闭触发条件 :虚拟机可因最后一个用户线程终止、调用 Runtime.exit 方法或外部环境信号而关闭。
  • 关闭钩子 :用于在虚拟机退出前执行清理操作,但要注意执行顺序和并发问题,避免死锁,且执行要快速。
  • 关闭序列 :先启动并执行所有关闭钩子线程,再调用 halt 方法终止虚拟机。避免在关闭过程中调用 exit ,防止无限阻塞。
  • 关闭策略 :尽量让程序正常完成,多线程程序可通过响应 interrupt 方法传达关闭请求,但对于依赖库创建的不响应中断的线程需特殊处理。仅在必要时调用 exit ,并确保所有线程有机会干净终止。

4.2 运行时类功能总结

  • 加载本地代码 :使用 Runtime 类的 loadLibrary load 方法加载本地代码库,加载操作需权限,否则会抛出异常。 System 类的 mapLibraryName 可进行库名映射。
  • 调试 Runtime 类的 traceInstructions traceMethodCalls 方法支持基本调试,高级调试可通过 JVM™ 工具接口(JVMTI)实现。

4.3 安全机制总结

  • 安全管理器 SecurityManager 类用于实现安全策略,通过 check 方法进行安全检查,将实际检查委托给访问控制对象。
  • 权限 :分为多个类别,不同类别由特定类管理,基本权限基于名称,部分权限有操作列表。
  • 安全策略 :由 Policy 对象表示,默认通过策略文件定义,受信任的类(如引导加载器加载的类)无需显式设置权限。
  • 访问控制器和特权执行 AccessController 类提供安全检查、上下文快照和特权执行功能。特权执行可让受信任代码临时获得更多操作权限,但使用时要谨慎。

4.4 实践建议

  • 运行时系统关闭
    • 在设计多线程应用程序时,提前规划好线程的关闭逻辑,确保每个线程能正确响应关闭请求。
    • 对于关闭钩子,编写单元测试来验证其执行顺序和清理操作的正确性。
    • 避免在关闭过程中进行复杂的操作或与用户交互,确保关闭过程快速稳定。
  • 运行时类功能使用
    • 在加载本地代码时,确保有足够的权限,并处理可能抛出的异常,如 SecurityException UnsatisfiedLinkError
    • 在开发环境中使用调试方法,结合高级调试工具进行全面的调试和性能分析。
  • 安全机制应用
    • 合理设置安全管理器,根据应用程序的需求定义不同的安全策略。
    • 仔细管理权限,避免过度授权,确保每个代码源只拥有必要的权限。
    • 在使用特权执行时,严格控制特权代码的范围和操作,防止安全漏洞。

4.5 流程图总结

graph TD;
    A[虚拟机关闭触发] --> B{触发方式};
    B -->|最后一个用户线程终止| C[启动关闭序列];
    B -->|调用Runtime.exit| C;
    B -->|外部环境信号| C;
    C --> D[启动所有关闭钩子线程];
    D --> E{所有钩子线程终止?};
    E -->|是| F[调用halt方法];
    E -->|否| G[关闭序列无法完成];
    G -->|内部启动| H[虚拟机不终止];
    G -->|外部信号| I[虚拟机可能被强制终止];
    F --> J[虚拟机停止运行];

4.6 表格总结

功能模块 关键方法 作用 注意事项
运行时系统关闭 Runtime.exit 显式关闭虚拟机 传入状态码,可能导致线程直接终止,无 finally 执行
addShutdownHook 注册关闭钩子 关闭过程开始后不能注册,注意执行顺序和并发问题
removeShutdownHook 注销关闭钩子 关闭过程开始后不能注销
halt 终止虚拟机 直接调用可能阻止未完成钩子清理,不建议
运行时类功能 - 加载本地代码 Runtime.loadLibrary 加载动态库 需权限,可能抛出异常
Runtime.load 加载指定文件为动态库 需权限,可能抛出异常
System.mapLibraryName 库名映射 系统相关
运行时类功能 - 调试 Runtime.traceInstructions 启用或禁用指令跟踪 输出依赖主机环境
Runtime.traceMethodCalls 启用或禁用方法调用跟踪 输出依赖主机环境
安全机制 - 安全管理器 SecurityManager.checkXXX 安全检查 委托给访问控制对象
System.setSecurityManager 设置安全管理器 需权限,可能替换现有管理器
System.getSecurityManager 获取安全管理器 未设置返回 null
安全机制 - 访问控制器 AccessController.checkPermission 安全检查 基于当前线程上下文
AccessController.doPrivileged 特权执行代码 需权限,注意控制范围

通过以上总结和建议,开发者可以更好地理解和应用 Java 运行时系统的关闭、功能扩展和安全机制,提高应用程序的稳定性、性能和安全性。在实际开发中,要根据具体需求灵活运用这些知识,并不断进行测试和优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值