在上一章中,我们了解到,Java 微基准线束(JMH)是一个 Java 线束库,用于为 JVM 编写基准测试。我们尝试使用 Maven 和 JMH 编写性能测试,以帮助说明使用新 Java 平台进行微基准标记的过程。我们从微基准概述开始,然后与 Maven 深入到微基准,回顾了基准选项,最后介绍了一些避免微基准陷阱的技术。
在本章中,我们将重点介绍对Process类和java.lang.ProcessHandleAPI 的更新。在 Java 的早期版本中,在 Java9 之前,用 Java 管理进程是很困难的。API 不够,有些功能不够,有些任务需要以特定于系统的方式来解决。例如,在 Java8 中,让进程访问自己的进程标识符(PID)是一项不必要的困难任务。
在本章中,我们将探讨编写一个利用 Java 的进程管理 API 管理其他进程的应用所需的必要知识。具体来说,我们将介绍以下内容:
- 进程简介
- 使用
ProcessHandle接口 - 查看示例进程控制器应用
本章以 Java11 为特色,Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
本章的源代码可以在 GitHub 的 URL 上找到。
在 Java 应用编程的上下文中,进程是操作系统中的执行单元。当你启动一个程序,你就启动了一个过程。当机器引导代码时,它做的第一件事就是执行引导过程。然后,此进程启动其他进程,这些进程将成为引导进程的子进程。这些子进程可能会启动其他进程。这样,当机器运行时,就会有进程树在运行。
当机器做某事时,它是在某个进程内执行的某个代码中完成的。操作系统还作为同时执行的多个进程运行。应用作为一个或多个进程执行。大多数应用都是作为一个进程运行的,但作为一个例子,Chrome 浏览器启动几个进程来执行所有渲染和网络通信操作,这些操作共同起到浏览器的作用
要更好地了解进程是什么,请启动 Windows 上的任务管理器或 OSX 上的活动监视器,然后单击“进程”选项卡。您将看到机器上当前存在的不同进程。使用这些工具,您可以查看进程的参数,并且可以逐个终止进程
单个进程为其工作分配了内存,不允许它们自由访问彼此的内存
操作系统调度的执行单元是线程。进程由一个或多个线程组成。这些线程由操作系统调度器调度,并在时隙中执行
对于每个操作系统,进程都有一个 PID,它是一个标识进程的数字。不能同时有两个进程共享同一 PID。当我们想要在操作系统中识别一个活动进程时,我们使用 PID。在 Linux 和其他类似 Unix 的操作系统上,kill命令终止进程。要传递给此程序的参数是要终止的进程的 PID。终止可以是优雅的。这有点像要求进程退出。如果进程决定不运行,它可以继续运行。
程序可以准备在收到此类请求时停止。例如,Java 应用可以添加一个调用Runtime.getRuntime().addShutdownHook(Thread t)方法的Thread对象。传递的线程应该在进程被要求停止时启动,这样线程就可以执行程序退出前必须执行的所有任务。不幸的是,不能保证线程会真正启动,这取决于实际的实现。
Java9 中引入了两个支持处理操作系统进程的新接口-ProcessHandle和ProcessHandle.Info。
ProcessHandle对象标识操作系统进程并提供管理该进程的方法。在以前的 Java 版本中,这只能通过特定于操作系统的方法使用 PID 来标识进程。这种方法的主要问题是,PID 只有在进程处于活动状态时才是唯一的。当一个进程完成时,操作系统可以自由地为一个新进程重用 PID。当我们使用 PID 检查一个进程是否仍在运行时,我们实际上是在用该 PID 检查一个活动进程。当我们检查进程时,它可能是活动的,但是下次程序查询进程状态时,它可能是另一个进程
桌面和服务器操作系统尽量不重用 PID 值。在某些嵌入式系统上,操作系统可能只使用 16 位值来存储 PID。当仅使用 16 位值时,PIDs 被重用的可能性更大。我们现在可以使用ProcessHandleAPI 来避免这个问题。我们可以接收ProcessHandle,也可以调用handle.is.Alive()方法。此方法将在进程完成时返回false。即使重用了 PID,这种方法也可以工作。
我们可以通过handle访问进程的 PID。handle.getPid()方法返回Long表示 PID 的数值,由于通过句柄访问进程更安全,因此该方法的重要性受到限制。当我们的代码想要将自己的信息提供给其他管理工具时,它可能会派上用场。
程序通常会创建一个以数字 PID 作为文件名的文件。某个程序不能在多个进程中运行可能是一个要求。在这种情况下,代码将自己的 PID 文件写入特定目录。如果具有该名称的 PID 文件已存在,则处理将停止。如果前一个进程崩溃或终止而没有删除 PID 文件,那么系统管理器可以轻松地删除该文件并启动新进程。如果程序挂起,那么如果 PID 已知,系统管理器可以很容易地终止死进程。
为了得到当前进程的 PID,可以使用调用链ProcessHandle.current().getPid()。
要获取有关进程的信息,我们需要访问进程的Info对象。可通过ProcessHandle获取。我们使用对handle.info()方法的调用来返回它。
Info接口定义了传递进程信息的查询方法。这些是:
command()返回Optional<String>,其中包含用于启动进程的命令arguments()返回Optional<String[]>,其中包含启动进程的命令后在命令行上使用的参数commandLine()返回包含整个命令行的Optional<String>startInstant()返回Optional<Instant>,它本质上表示进程开始的时间totalCpuDuration()返回Optional<Duration>,表示进程自启动以来占用的 CPU 时间user()返回Optional<String>,其中包含进程所属的用户的名称
这些方法返回的值都是Optional,因为不能保证操作系统或 Java 实现能够返回信息,但是在大多数操作系统上,它应该工作,并且返回的值应该存在。
以下示例代码显示给定进程的信息:
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
public class ProcessHandleDemonstration {
public static void main(String[] args) throws InterruptedException,
IOException {
provideProcessInformation(ProcessHandle.current());
Process theProcess = new
ProcessBuilder("SnippingTool.exe").start();
provideProcessInformation(theProcess.toHandle());
theProcess.waitFor();
provideProcessInformation(theProcess.toHandle());
}
static void provideProcessInformation(ProcessHandle theHandle) {
// get id
long pid = ProcessHandle.current().pid();
// Get handle information (if available)
ProcessHandle.Info handleInformation = theHandle.info();
// Print header
System.out.println("|=============================|");
System.out.println("| INFORMATION ON YOUR PROCESS |");
System.out.println("|=============================|\n");
// Print the PID
System.out.println("Process id (PID): " + pid);
System.out.println("Process Owner: " +
handleInformation.user().orElse(""));
// Print additional information if available
System.out.println("Command:" +
handleInformation.command().orElse(""));
String[] args = handleInformation.arguments().orElse (new String[]{});
System.out.println("Argument(s): ");
for (String arg: args) System.out.printf("\t" + arg);
System.out.println("Command line: " +
handleInformation.commandLine().orElse(""));
System.out.println("Start time: " +
handleInformation.startInstant().orElse(Instant.now()).
toString());
System.out.printf("Run time duration: %sms%n",
handleInformation.totalCpuDuration().
orElse(Duration.ofMillis(0)).toMillis());
}
}以下是前面代码的控制台输出:
在 Java9 之前,我们没有获得活动进程列表的方法。使用 Java9、10 和 11,可以在Stream中获取进程。有三种方法返回Stream<ProcessHandle>,用于:
- 列出子进程
- 列出所有子项
- 列出所有进程
下一节将对每一项进行回顾。
为了得到控制子进程的进程句柄的Stream,应该使用静态方法processHandle.children()。这将创建processHandle表示的进程的子进程的快照,并创建Stream,由于进程是动态的,因此不能保证在代码执行过程中,当我们的程序处理句柄时,所有子进程仍然是活动的。它们中的一些可能会终止,而我们的进程可能会产生新的子进程,可能来自不同的线程。因此,代码不应该假设Stream的ProcessHandle元素代表一个活动的、正在运行的进程
以下程序在 Windows 中启动 10 个命令提示,然后计算子进程的数量并将其打印到标准输出:
import java.io.IOException;
public class ChildLister {
public static void main(String[] args) throws IOException {
for (int i = 0; i < 10; i++) {
new ProcessBuilder().command("cmd.exe").start();
}
System.out.println("Number of children :" +
ProcessHandle.current().children().count());
}
}执行该程序将导致以下结果:
列出子进程与列出子进程非常相似,但是如果我们调用processHandle.descendants()方法,那么Stream将包含所有子进程以及这些进程的子进程,依此类推。
以下程序以命令行参数启动命令提示,以便它们也生成另一个终止的cmd.exe:
import java.io.IOException;
import java.util.stream.Collectors;
public class DescendantLister {
public static void main(String[] args) throws IOException {
for (int i = 0; i < 10; i++) {
new ProcessBuilder().command("cmd.exe","/K","cmd").start();
}
System.out.println("Number of descendants: " +
ProcessHandle.current().descendants().count();
}
}多次运行该命令将导致以下不确定的输出:
输出清楚地表明,当子进程的Stream被创建时,并不是所有进程都是活动的。示例代码启动 10 个进程,每个进程启动另一个进程。Stream没有 20 个元素,因为其中一些子进程在处理过程中被终止。
列出所有进程与列出子进程和子进程略有不同。方法allProcess()是静态的,返回执行时操作系统中所有活动进程的句柄Stream。
以下示例代码将进程命令打印到控制台,这些命令看起来像是 Java 进程:
import java.lang.ProcessHandle.Info;
public class ProcessLister {
private static void out(String format, Object... params) {
System.out.println(String.format(format, params));
}
private static boolean looksLikeJavaProcess(Info info) {
return info.command().isPresent() && info.command().get().
toLowerCase().indexOf("java") != -1;
}
public static void main(String[] args) {
ProcessHandle.allProcesses().map(ProcessHandle::info).
filter(info -> looksLikeJavaProcess(info)).
forEach((info) -> System.out.println(info.command().
orElse("---")));
}
}程序的输出列出了所有内有字符串java的过程命令,如下所示:
当然,您的实际输出可能不同。
当一个进程启动另一个进程时,它可能会多次等待该进程,因为它需要另一个程序的结果。如果任务的结构可以这样组织,即父程序可以在等待子进程完成时执行其他操作,则父进程可以调用进程句柄上的isAlive()方法。通常,在派生的进程完成之前,父进程无事可做。遗留应用实现了调用Thread.sleep()方法的循环,这样 CPU 就不会过度浪费,进程会定期检查,看它是否还活着
当前的 Java 平台提供了一种更好的方法来处理等待过程,ProcessHandle接口有一个名为onExit()的方法返回CompletableFuture。这个类可以在不循环的情况下等待任务完成。如果我们有一个进程的句柄,我们可以简单地调用handle.onExit().join()方法等待进程完成。返回的CompletableFuture的get()方法返回最初用于创建它的ProcessHandle实例
我们可以多次调用句柄上的onExit()方法,每次它都会返回不同的CompletableFuture对象,每个对象都与同一进程相关。我们可以在对象上调用cancel()方法,但它只会取消CompletableFuture对象,而不会取消进程,并且不会对从同一ProcessHandle实例创建的其他CompletableFuture对象产生任何影响。
要终止一个进程,我们可以在ProcessHandle实例上调用destroy()方法或destroyForcibly()方法。这两种方法都将终止进程,destroy()方法将终止进程,优雅地执行进程关闭序列。在这种情况下,如果实际实现支持进程的正常终止,那么将执行添加到运行时的关闭挂钩。
destroyForcibly()方法将强制进程终止,在这种情况下,将不执行关闭序列。如果句柄管理的进程不活动,则代码调用这些方法时不会发生任何事情。如果在句柄上创建了调用onExit()方法的CompletableFuture对象,则当进程终止时,在调用destroy()或destroyForcefully()方法后,这些对象将完成。
这意味着CompletableFuture对象将在进程结束一段时间后从join()或类似方法返回,而不是在destroy()或destroyForcefully()返回之后立即返回
同样重要的是要注意,进程终止可能取决于许多事情。如果等待终止另一个进程的实际进程无权终止另一个进程,则请求将失败。在这种情况下,方法的返回值是false。返回值true并不意味着进程实际上已经终止。这只意味着操作系统接受了终止请求,并且操作系统将在将来的某个时候终止进程。这实际上很快就会发生,但不是瞬间发生的,因此如果方法isAlive()在destroy()或destroyForcefully()方法返回值true之后的一段时间内返回true,也就不足为奇了。
destroy()和destroyForcefully()之间的区别是具体实现的。Java 标准没有规定destroy()终止让关闭序列执行的进程。它只请求终止进程。此ProcessHandle对象表示的进程是否正常终止取决于实现。
这是因为某些操作系统没有实现优雅的进程终止特性。在这种情况下,destroy()的实现与调用destroyForcefully()相同。接口ProcessHandle的系统特定实现必须实现方法supportsNormalTermination(),只有当实现支持正常(非强制)进程终止时,才应该是true。对于实际实现中的所有调用,该方法应返回相同的值,并且在执行 JVM 实例期间不应更改返回值。不需要多次调用该方法。
下面的示例演示了进程启动、进程终止和等待进程终止。在我们的示例中,我们使用两个类。第一个类演示了.sleep()方法:
public class WaitForChildToBeTerminated {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(10_000);
}
}我们示例中的第二个类称为WaitForChildToBeTerminated类:
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class TerminateAProcessAfterWaiting {
private static final int N = 10;
public static void main(String[] args) throws IOException,
InterruptedException {
ProcessHandle ph[] = new ProcessHandle[N];
for (int i = 0; i < N; i++) {
final ProcessBuilder pb = ew ProcessBuilder().
command("java", "-cp", "build/classes/main",
"packt.mastering.java11.process.WaitForChildToBeTerminated");
Process p = pb.start();
ph[i] = p.toHandle();
}
long start = System.currentTimeMillis();
Arrays.stream(ph).forEach(ProcessHandle::destroyForcibly);
CompletableFuture.allOf(Arrays.stream(ph).
map(ProcessHandle::onExit).collect(Collectors.toList()).
toArray(new CompletableFuture[ph.length])).join();
long duration = System.currentTimeMillis() - start;
System.out.println("Duration " + duration + "ms");
}
}前面的代码启动 10 个进程,每个进程执行休眠 10 秒的程序。它强制销毁进程,或者更具体地说,要求操作系统销毁进程。我们的示例连接了由CompletableFuture对象数组组成的CompletableFuture,这些对象是使用各个进程的句柄创建的
当所有进程完成后,它以毫秒为单位打印出测量的时间。时间间隔从进程创建和进程创建循环完成时开始。当 JVM 从join()方法返回时,当进程被识别时,测量的时间间隔结束
示例代码将睡眠时间设置为 10 秒。这是一个更明显的时间段。运行两次代码并删除破坏进程的行会导致打印速度慢得多。实际上,测量和打印的运行时间也会显示终止进程会产生影响。
最后一节提供了一个示例过程控制应用来演示本章的内容。应用的功能非常简单。它从一系列配置文件参数中读取如何启动某些进程,然后,如果其中任何进程停止,它将尝试重新启动进程。
这个示例应用可以作为实际应用的起点。可以使用环境变量规范扩展进程的参数集。您还可以为进程、输入和输出重定向添加一个默认目录,甚至还可以添加一个进程的 CPU 消耗量,而无需控制应用终止并重新启动它
应用由四个类组成:
Main:此类包含public static void main方法,用于启动守护进程。Parameters:此类包含进程的配置参数。在这个简单的例子中,它只包含一个字段,即命令行。如果应用得到扩展,这个类将包含默认目录、重定向和 CPU 使用限制数据。ParamsAndHandle:这个类只不过是一个数据元组,其中包含对Parameters对象的引用,同时也是一个进程句柄。当一个进程死亡并重新启动时,进程句柄将被新的句柄替换,但是对Parameters对象的引用不会改变它的配置ControlDaemon:这个类实现了Runnable接口,作为一个单独的线程启动。
main()方法从命令行参数中获取目录名。它将此视为相对于当前工作目录。它使用同一类中的单独方法从目录中的文件读取配置集,然后启动控制守护进程。以下代码是程序的main()方法:
public static void main(String[] args) throws IOException,
InterruptedException {
// DemoOutput.out() simulated - implementation not shown
DemoOutput.out(new File(".").getAbsolutePath().toString());
if (args.length == 0) {
System.err.println("Usage: daemon directory");
System.exit(-1);
}
Set<Parameters> params = parametersSetFrom(args[0]);
Thread t = new Thread(new ControlDaemon(params));
t.start();
}虽然这是一个守护进程,但我们将它作为普通线程而不是守护线程启动。当一个线程被设置为守护线程时,它不会使 JVM 保持活动状态。当所有其他非守护线程停止时,JVM 将退出,守护线程将停止。在我们的例子中,我们执行的守护线程是保持代码运行的唯一线程。在启动之后,主线程就没有什么事情可做了,但是 JVM 应该保持活动状态,直到运算符发出 Unixkill命令或在命令行上按Ctrl + C将其杀死。
使用 JDK 中新的Files和Paths类,获取指定目录中的文件列表并从文件中获取参数非常简单:
private static Set<Parameters>
GetListOfFilesInDirectory(String directory) throws IOException {
return Files.walk(Paths.get(directory)).map(Path::toFile)
.filter(File::isFile).map(file -> Parameters.fromFile(file))
.collect(Collectors.toSet());
}我们得到一个以Path对象形式出现的文件流,将其映射到File对象,然后过滤出configuration目录中的目录,并使用静态方法将剩余的普通文件从Parameters类的File映射到Parameters对象。最后,我们返回对象的Set。
我们的Parameters类有一个字段和一个构造器,如下所示:
final String[] commandLine;
public Parameters(String[] commandLine) {
this.commandLine = commandLine;
}Parameters类有两个方法。第一个方法getCommandLineStrings()从属性中检索命令行字符串。此数组包含命令和命令行参数。如果文件中没有定义,则返回一个空数组,如下所示:
private static String[] getCommandLineStrings(Properties props) {
return Optional.ofNullable(props.getProperty("commandLine"))
.orElse("").split("\\s+");
}第二种方法是静态的fromFile(),它从properties文件中读取属性,如下所示:
public static Parameters fromFile(final File file) {
final Properties props = new Properties();
try (final InputStream is = new FileInputStream(file)) {
props.load(is);
} catch (IOException e) {
throw new RuntimeException(e);
}
return new Parameters(getCommandLineStrings(props));
}如果程序处理的参数集被扩展,那么这个类也应该被修改。
ParamsAndHandle是一个非常简单的类,它包含两个字段,一个是参数字段,另一个是进程句柄的句柄,用于访问使用参数启动的进程,如下所示:
public class ParamsAndHandle {
final Parameters params;
ProcessHandle handle;
public ParamsAndHandle(Parameters params,ProcessHandle handle) {
this.params = params;
this.handle = handle;
}
public ProcessHandle toHandle() {
return handle;
}
}由于该类与使用它的ControlDaemon类紧密相连,因此没有与该字段相关联的更改器或访问器。我们把这两个类看作是在同一个封装边界内的东西。toHandle () 方法就在那里,所以我们可以将它用作方法句柄,我们将在第 10 章、“细粒度栈跟踪”中看到。
ControlDaemon类实现Runnable接口,并作为单独的线程启动。构造器获取从属性文件读取的参数集,并将其转换为一组ParamsAndHandle对象,如下所示:
private final Set<ParamsAndHandle> handlers;
public ControlDaemon(Set<Parameters> params) {
handlers = params.stream()
.map( s -> new ParamsAndHandle(s,null))
.collect(Collectors.toSet());
}因为此时没有启动进程,所以句柄都是null。使用run()方法启动进程,如下所示:
@Override
public void run() {
try {
for (ParamsAndHandle pah : handlers) {
log.log(DEBUG, "Starting {0}", pah.params);
ProcessHandle handle = start(pah.params);
pah.handle = handle;
}
keepProcessesAlive();
while (handlers.size() > 0) {
allMyProcesses().join();
}
} catch (IOException e) {
log.log(ERROR, e);
}
}处理遍历参数集并使用方法(稍后在此类中实现)启动进程。每个进程的句柄到达ParamsAndHandle对象。之后,调用keepProcessesAlive()方法并等待进程完成。当进程停止时,它就会重新启动。如果不能重新启动,它将从集合中删除
allMyProcesses()方法(也在这个类中实现)返回一个CompletableFuture,当所有启动的进程都停止时,该方法就会完成。当join()方法返回时,一些进程可能已经重新启动。只要至少有一个进程在运行,线程就应该运行。
使用CompletableFuture等待进程和while循环,只要至少有一个进程可以运行,我们就使用最少的 CPU 来保持线程的活动性,可能甚至在重新启动几次之后。我们必须让这个线程保持活动状态,即使它大部分时间不使用 CPU,也不执行代码,以便让keepProcessesAlive()方法使用CompletableFutures完成工作。该方法显示在以下代码段中:
private void keepProcessesAlive() {
anyOfMyProcesses().thenAccept(ignore -> {
restartProcesses();
keepProcessesAlive();
});
}keepProcessesAlive()方法调用返回CompletableFuture的anyOfMyProcesses()方法,该方法在任何托管进程退出时完成。方法计划在完成CompletableFuture时执行作为参数传递给thenAccept()方法的 Lambda。Lambda 做了两件事:
- 重新启动已停止的进程(可能只有一个)
- 调用
keepProcessesAlive()方法
重要的是要理解这个调用不是从keepProcessesAlive()方法本身执行的。这不是递归调用。这被安排为一个CompletableFuture动作。我们不是在递归调用中实现循环,因为我们会耗尽栈空间。我们要求 JVM 执行者在进程重新启动时再次执行这个方法。
JVM 使用默认的ForkJoinPool来调度这些任务,这个池包含守护线程。这就是我们必须等待并保持方法运行的原因,因为这是唯一阻止 JVM 退出的非守护线程。
下一种方法是restartProcesses(),如下所示:
private void restartProcesses() {
Set<ParamsAndHandle> failing = new HashSet<>();
handlers.stream()
.filter(pah -> !pah.toHandle().isAlive())
.forEach(pah -> {
try {
pah.handle = start(pah.params);
} catch (IOException e) {
failing.add(pah);
}
});
handlers.removeAll(failing);
}此方法启动我们的托管进程集中且不存在的进程。如果任何重新启动失败,它将从集合中删除失败的进程。(注意不要在回路中取出,以免ConcurrentModificationException。anyOfMyProcesses()和allMyProcesses()方法采用辅助completableFuturesOfTheProcessesand()方法,简单明了,如下:
private CompletableFuture anyOfMyProcesses() {
return CompletableFuture.anyOf(
completableFuturesOfTheProcesses());
}
private CompletableFuture allMyProcesses() {
return CompletableFuture.allOf(
completableFuturesOfTheProcesses());
}completableFuturesOfTheProcesses()方法返回从当前运行的托管进程调用其onExit()方法创建的CompletableFutures数组。这是以简洁易读的函数式编程风格完成的,如下所示:
private CompletableFuture[] completableFuturesOfTheProcesses() {
return handlers.stream()
.map(ParamsAndHandle::toHandle)
.map(ProcessHandle::onExit)
.collect(Collectors.toList())
.toArray(new CompletableFuture[handlers.size()]);
}集合被转换成一个Stream,映射到ProcessHandle对象的Stream(这就是为什么我们需要ParamsAndHandle类中的toHandle()方法)。然后使用onExit()方法将句柄映射到CompletableFuture流,最后将其收集到列表并转换为数组。
我们完成示例应用的最后一种方法如下:
private ProcessHandle start(Parameters params)
throws IOException {
return new ProcessBuilder(params.commandLine)
.start().toHandle();
}此方法使用ProcessBuilder启动进程并返回ProcessHandle,以便替换集合中的旧进程并管理新进程。
在本章中,我们讨论了当前的 Java 平台如何使我们能够管理进程。这与早期版本的 Java 相比有了很大的改进,后者需要特定于操作系统的实现,而且在 CPU 使用和编码实践方面还不够理想。现代的 API,加上像ProcessHandle这样的新类,使得处理进程的几乎所有方面成为可能。我们还构建了一个完整的应用,管理将学到的 API 付诸实践的进程。
在下一章中,我们将详细介绍 Java 栈遍历 API。我们将使用代码示例来说明如何使用 API。
-
什么是进程?
-
哪两个接口支持处理操作系统进程?
-
当一个进程结束时,什么方法返回
false? -
如何访问进程的 PID?
-
如何检索当前进程的 PID?
-
列出
Info接口用于传递进程信息的六种查询方法。 -
用什么方法得到控制子进程的进程句柄的
Stream? -
使用什么方法来获取子体的进程句柄的
Stream? -
什么方法可以用来检索所有子代和子代的列表?
-
onExit()方法返回什么?
有关详细信息,请访问以下链接:
- 《Java9 高性能》在这个页面提供。



