Tips
做一個終身學習的人。

在本章中,主要介紹以下內容:
- Process API是什么
- 如何創建本地進程
- 如何獲取新進程的信息
- 如何獲取當前進程的信息
- 如何獲取所有系統進程的信息
- 如何設置創建,查詢和管理本地進程的權限
一. Process API是什么
Process API 由接口和類組成,用來與本地進程一起工作,使用API,可以做以下事情:
- 從Java代碼中創建新的本地進程
- 獲取本地進程的進程句柄,無論它們是由Java代碼還是通過其他方式創建
- 銷毀運行進程
- 查詢活動的進程及其屬性
- 獲取進程的子進程和父進程的列表
- 獲取本地進程的進程ID(PID)
- 獲取新創建的進程的輸入,輸出和錯誤流
- 等待進程終止
- 當進程終止時執行任務
Process API由java.lang包中的以下類和接口組成:
Runtime
ProcessBuilder
ProcessBuilder.Redirect
Process
ProcessHandle
ProcessHandle.Info
自Java 1.0以來,支持使用本地進程。Process類的實例表示由Java程序創建的本地進程。 通過調用Runtime類的exec()方法啟動一個進程。
JDK 5.0添加了ProcessBuilder類,JDK 7.0添加了ProcessBuilder.Redirect的嵌套類。 ProcessBuilder類的實例保存一個進程的一組屬性。 調用其start()方法啟動本地進程並返回一個表示本地進程的Process類的實例。 可以多次調用其start()方法; 每次使用ProcessBuilder實例中保存的屬性啟動一個新進程。 在Java 5.0中,ProcessBuilder類接管Runtime.exec()方法來啟動新進程。
在Java 7和Java 8中的Process API中有一些改進,就是在Process和ProcessBuilder類中添加幾個方法。
在Java 9之前,Process API仍然缺乏對使用本地進程的基本支持,例如獲取進程的PID和所有者,進程的開始時間,進程使用了多少CPU時間,多少本地進程正在運行等。請注意,在Java 9之前,可以啟動本地進程並使用其輸入,輸出和錯誤流。 但是,無法使用未啟動的本地進程,無法查詢進程的詳細信息。 為了更緊密地處理本地進程,Java開發人員不得不使用Java Native Interface(JNI)來編寫本地代碼。 Java 9使這些非常需要的功能與本地進程配合使用。
Java 9向Process API添加了一個名為ProcessHandle的接口。 ProcessHandle接口的實例標識一個本地進程; 它允許查詢進程狀態並管理進程。
比較Process類和ProcessHandle接口。 Process類的一個實例表示由當前Java程序啟動的本地進程,而ProcessHandle接口的實例表示本地進程,無論是由當前Java程序啟動還是以其他方式啟動。 在Java 9中,已經在Process類中添加了幾種方法,這些方法也可以在新的ProcessHandle接口中使用。 Process類包含一個返回ProcessHandle的toHandle()方法。
ProcessHandle.Info接口的實例表示進程屬性的快照。 請注意,進程由不同的操作系統不同地實現,因此它們的屬性不同。 過程的狀態可以隨時更改,例如,當進程獲得更多CPU時間時,進程使用的CPU時間增加。 要獲取進程的最新信息,需要在需要時使用ProcessHandle接口的info()方法,這將返回一個新的ProcessHandle.Info實例。
本章中的所有示例都在Windows 10中運行。當使用Windows 10或其他操作系統在機器上運行這些程序時,可能會得到不同的輸出。
二. 當前進程
ProcessHandle接口的current()靜態方法返回當前進程的句柄。 請注意,此方法返回的當前進程始終是正在執行代碼的Java進程。
// Get the handle of the current process
ProcessHandle current = ProcessHandle.current();
獲取當前進程的句柄后,可以使用ProcessHandle接口的方法獲取有關進程的詳細信息。
Tips
你不能殺死當前進程。 嘗試通過使用ProcessHandle接口的destroy()或destroyForcibly()方法來殺死當前進程會導致IllegalStateException異常。
三. 查詢進程狀態
可以使用ProcessHandle接口中的方法來查詢進程的狀態。 下表列出了該接口常用的簡單說明方法。 請注意,許多這些方法返回執行快照時進程狀態的快照。 不過,由於進程是以異步方式創建,運行和銷毀的,所以當稍后使用其屬性時,所以無法保證進程仍然處於相同的狀態。
| 方法 | 描述 |
|---|---|
static Stream<ProcessHandle> allProcesses() |
返回操作系統中當前進程可見的所有進程的快照。 |
Stream<ProcessHandle> children() |
返回進程當前直接子進程的快照。 使用descendants()方法獲取所有級別的子級列表,例如子進程,孫子進程進程等。返回當前進程可見的操作系統中的所有進程的快照。 |
static ProcessHandle current() |
返回當前進程的ProcessHandle,這是執行此方法調用的Java進程。 |
Stream<ProcessHandle> descendants() |
返回進程后代的快照。 與children()方法進行比較,該方法僅返回進程的直接后代。 |
boolean destroy() |
請求進程被殺死。 如果成功請求終止進程,則返回true,否則返回false。 是否可以殺死進程取決於操作系統訪問控制。 |
boolean destroyForcibly() |
要求進程被強行殺死。 如果成功請求終止進程,則返回true,否則返回false。 殺死進程會立即強制終止進程,而正常終止則允許進程徹底關閉。 是否可以殺死進程取決於操作系統訪問控制。 |
long getPid() |
返回由操作系統分配的進程的本地進程ID(PID)。 注意,PID可以由操作系統重復使用,因此具有相同PID的兩個處理句柄可能不一定代表相同的過程。 |
ProcessHandle.Info info() |
返回有關進程信息的快照。 |
boolean isAlive() |
如果此ProcessHandle表示的進程尚未終止,則返回true,否則返回false。 請注意,在成功請求終止進程后,此方法可能會返回一段時間,因為進程將以異步方式終止。 |
static Optional<ProcessHandle> of(long pid) |
返回現有本地進程的Optional<ProcessHandle>。 如果具有指定pid的進程不存在,則返回空的Optional。 |
CompletableFuture <ProcessHandle> onExit() |
返回一個用於終止進程的CompletableFuture<ProcessHandle>。 可以使用返回的對象來添加在進程終止時執行的任務。 在當前進程中調用此方法會引發IllegalStateException異常。 |
Optional<ProcessHandle> parent() |
返回父進程的Optional<ProcessHandle>。 |
boolean supportsNormalTermination() |
如果destroy()的實現正常終止進程,則返回true。 |
下表列出ProcessHandle.Info嵌套接口的方法和描述。 此接口的實例包含有關進程的快照信息。 可以使用ProcessHandle接口或Process類的info()方法獲取ProcessHandle.Info。 接口中的所有方法都返回一個Optional。
| 方法 | 描述 |
|---|---|
Optional<String[]> arguments() |
返回進程的參數。 該過程可能會更改啟動后傳遞給它的原始參數。 在這種情況下,此方法返回更改的參數。 |
Optional<String> command() |
返回進程的可執行路徑名。 |
Optional<String> commandLine() |
它是一個進程的組合命令和參數的便捷的方法。如果command()和arguments()方法都沒有返回空Optional, 它通過組合從command()和arguments()方法返回的值來返回進程的命令行。 |
Optional<Instant> startInstant() |
返回進程的開始時間。 如果操作系統沒有返回開始時間,則返回一個空Optional。 |
Optional<Duration> totalCpuDuration() |
返回進程使用的CPU時間。 請注意,進程可能運行很長時間,但可能使用很少的CPU時間。 |
Optional<String> user() |
返回進程的用戶。 |
現在是時候看到ProcessHandle和ProcessHandle.Info接口的實際用法。 本章中的所有類都在com.jdojo.process.api模塊中,其聲明如下所示。
// module-info.java
module com.jdojo.process.api {
exports com.jdojo.process.api;
}
接下來包含CurrentProcessInfo類的代碼。 它的printInfo()方法將ProcessHandle作為參數,並打印進程的詳細信息。 我們還在其他示例中使用此方法打印進程的詳細信息。main()方法獲取運行進程的當前進程的句柄,這是一個Java進程,並打印其詳細信息。 你可能會得到不同的輸出。 以下是當程序在Windows 10上運行時生成輸出。
// CurrentProcessInfo.java
package com.jdojo.process.api;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
public class CurrentProcessInfo {
public static void main(String[] args) {
// Get the handle of the current process
ProcessHandle current = ProcessHandle.current();
// Print the process details
printInfo(current);
}
public static void printInfo(ProcessHandle handle) {
// Get the process ID
long pid = handle.getPid();
// Is the process still running
boolean isAlive = handle.isAlive();
// Get other process info
ProcessHandle.Info info = handle.info();
String command = info.command().orElse("");
String[] args = info.arguments()
.orElse(new String[]{});
String commandLine = info.commandLine().orElse("");
ZonedDateTime startTime = info.startInstant()
.orElse(Instant.now())
.atZone(ZoneId.systemDefault());
Duration duration = info.totalCpuDuration()
.orElse(Duration.ZERO);
String owner = info.user().orElse("Unknown");
long childrenCount = handle.children().count();
// Print the process details
System.out.printf("PID: %d%n", pid);
System.out.printf("IsAlive: %b%n", isAlive);
System.out.printf("Command: %s%n", command);
System.out.printf("Arguments: %s%n", Arrays.toString(args));
System.out.printf("CommandLine: %s%n", commandLine);
System.out.printf("Start Time: %s%n", startTime);
System.out.printf("CPU Time: %s%n", duration);
System.out.printf("Owner: %s%n", owner);
System.out.printf("Children Count: %d%n", childrenCount);
}
}
打印輸出為:
PID: 8692
IsAlive: true
Command: C:\java9\bin\java.exe
Arguments: []
CommandLine:
Start Time: 2016-11-27T12:28:20.611-06:00[America/Chicago]
CPU Time: PT0.296875S
Owner: kishori\ksharan
Children Count: 1
四. 比較進程
比較兩個進程是否相等等或順序是否相同是棘手的。 不能依賴PID來處理相同的進程。 操作系統在進程終止后重用PID。 可以與PID一起檢查流程的開始時間;如果兩者相同,則兩個過程可能相同。 ProcessHandle接口的默認實現的equals()方法檢查以下三個信息,以使兩個進程相等:
- 對於這兩個進程,
ProcessHandle接口的實現類必須相同。 - 進程必須具有相同的PID。
- 進程必須同一時間啟動。
Tips
在ProcessHandle接口中使用compareTo()方法的默認實現對於排序來說並不是很有用。 它比較了兩個進程的PID。
五. 創建進程
需要使用ProcessBuilder類的實例來啟動一個新進程。 該類包含幾個方法來設置進程的屬性。 調用start()方法啟動一個新進程。 start()方法返回一個Process對象,可以使用它來處理進程的輸入,輸出和錯誤流。 以下代碼段創建一個ProcessBuilder在Windows上啟動JVM:
ProcessBuilder pb = new ProcessBuilder()
.command("C:\\java9\\bin\\java.exe",
"--module-path",
"myModulePath",
"--module",
"myModule/className")
.inheritIO();
有兩種方法來設置這個新進程的命令和參數:
- 可以將它們傳遞給
ProcessBuilder類的構造函數。 - 可以使用
command()方法。
沒有參數的command()方法返回在ProcessBuilder中命令的設置的。 帶有參數的其他版本 —— 一個帶有一個String的可變參數,一個帶有List<String>的版本,都用於設置命令和參數。 該方法的第一個參數是命令路徑,其余的是命令的參數。
新進程有自己的輸入,輸出和錯誤流。 inheritIO()方法將新進程的輸入,輸出和錯誤流設置為與當前進程相同。 ProcessBuilder類中有幾個redirectXxx()方法可以為新進程定制標准I/O,例如將標准錯誤流設置為文件,因此所有錯誤都會記錄到文件中。 配置進程的所有屬性后,可以調用start()來啟動進程:
// Start a new process
Process newProcess = pb.start();
可以多次調用ProcessBuilder類的start()方法來啟動與之前保持的相同屬性的多個進程。 這具有性能優勢,可以創建一個ProcessBuilder實例,並重復使用它來多次啟動相同的進程。
可以使用Process類的toHandle()方法獲取進程的進程句柄:
// Get the process handle
ProcessHandle handle = newProcess.toHandle();
可以使用進程句柄來銷毀進程,等待進程完成,或查詢進程的狀態和屬性,如其子進程,后代,父進程,使用的CPU時間等。有關進程的信息,對進程的控制取決於操作系統訪問控制。
創建可以在所有操作系統上運行的進程都很棘手。 可以創建一個新進程啟動新的JVM來運行一個類。
如下包含一個Job類的代碼。 它的main()方法需要兩個參數:睡眠間隔和睡眠持續時間(以秒為單位)。 如果沒有參數傳遞,該方法將使用5秒和60秒作為默認值。 在第一部分中,該方法嘗試提取第一個和第二個參數(如果指定)。 在第二部分中,它使用ProcessHandle.current()方法獲取當前進程執行此方法的進程句柄。 它讀取當前進程的PID並打印包括PID,睡眠間隔和睡眠持續時間的消息。 最后,它開始一個for循環,並持續休眠睡眠間隔,直到達到睡眠持續時間。 在循環的每次迭代中,它打印一條消息。
// Job.java
package com.jdojo.process.api;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* An instance of this class is used as a job that sleeps at a
* regular interval up to a maximum duration. The sleep
* interval in seconds can be specified as the first argument
* and the sleep duration as the second argument while running.
* this class. The default sleep interval and sleep duration
* are 5 seconds and 60 seconds, respectively. If these values
* are less than zero, zero is used instead.
*/
public class Job {
// The job sleep interval
public static final long DEFAULT_SLEEP_INTERVAL = 5;
// The job sleep duration
public static final long DEFAULT_SLEEP_DURATION = 60;
public static void main(String[] args) {
long sleepInterval = DEFAULT_SLEEP_INTERVAL;
long sleepDuration = DEFAULT_SLEEP_DURATION;
// Get the passed in sleep interval
if (args.length >= 1) {
sleepInterval = parseArg(args[0], DEFAULT_SLEEP_INTERVAL);
if (sleepInterval < 0) {
sleepInterval = 0;
}
}
// Get the passed in the sleep duration
if (args.length >= 2) {
sleepDuration = parseArg(args[1], DEFAULT_SLEEP_DURATION);
if (sleepDuration < 0) {
sleepDuration = 0;
}
}
long pid = ProcessHandle.current().getPid();
System.out.printf("Job (pid=%d) info: Sleep Interval" +
"=%d seconds, Sleep Duration=%d " +
"seconds.%n",
pid, sleepInterval, sleepDuration);
for (long sleptFor = 0; sleptFor < sleepDuration;
sleptFor += sleepInterval) {
try {
System.out.printf("Job (pid=%d) is going to" +
" sleep for %d seconds.%n",
pid, sleepInterval);
// Sleep for the sleep interval
TimeUnit.SECONDS.sleep(sleepInterval);
} catch (InterruptedException ex) {
System.out.printf("Job (pid=%d) was " +
"interrupted.%n", pid);
}
}
}
/**
* Starts a new JVM to run the Job class.
* @param sleepInterval The sleep interval when the Job
* class is run. It is passed to the JVM as the first
* argument.
* @param sleepDuration The sleep duration for the Job
* class. It is passed to the JVM as the second argument.
* @return The new process reference of the newly launched
* JVM or null if the JVM cannot be launched.
*/
public static Process startProcess(long sleepInterval,
long sleepDuration) {
// Store the command to launch a new JVM in a
// List<String>
List<String> cmd = new ArrayList<>();
// Add command components in order
addJvmPath(cmd);
addModulePath(cmd);
addClassPath(cmd);
addMainClass(cmd);
// Add arguments to run the class
cmd.add(String.valueOf(sleepInterval));
cmd.add(String.valueOf(sleepDuration));
// Build the process attributes
ProcessBuilder pb = new ProcessBuilder()
.command(cmd)
.inheritIO();
String commandLine = pb.command()
.stream()
.collect(Collectors.joining(" "));
System.out.println("Command used:\n" + commandLine);
// Start the process
Process p = null;
try {
p = pb.start();
} catch (IOException e) {
e.printStackTrace();
}
return p;
}
/**
* Used to parse the arguments passed to the JVM, which
* in turn is passed to the main() method.
* @param valueStr The string value of the argument
* @param defaultValue The default value of the argument if
* the valueStr is not an integer.
* @return valueStr as a long or the defaultValue if
* valueStr is not an integer.
*/
private static long parseArg(String valueStr,
long defaultValue) {
long value = defaultValue;
if (valueStr != null) {
try {
value = Long.parseLong(valueStr);
} catch (NumberFormatException e) {
// no action needed
}
}
return value;
}
/**
* Adds the JVM path to the command list. It first attempts
* to use the command attribute of the current process;
* failing that it relies on the java.home system property.
* @param cmd The command list
*/
private static void addJvmPath(List<String> cmd) {
// First try getting the command to run the current JVM
String jvmPath = ProcessHandle.current()
.info()
.command().orElse("");
if(jvmPath.length() > 0) {
cmd.add(jvmPath);
} else {
// Try composing the JVM path using the java.home
// system property
final String FILE_SEPARATOR =
System.getProperty("file.separator");
jvmPath = System.getProperty("java.home") +
FILE_SEPARATOR + "bin" +
FILE_SEPARATOR + "java";
cmd.add(jvmPath);
}
}
/**
* Adds a module path to the command list.
* @param cmd The command list
*/
private static void addModulePath(List<String> cmd) {
String modulePath =
System.getProperty("jdk.module.path");
if(modulePath != null && modulePath.trim().length() > 0) {
cmd.add("--module-path");
cmd.add(modulePath);
}
}
/**
* Adds class path to the command list.
* @param cmd The command list
*/
private static void addClassPath(List<String> cmd) {
String classPath = System.getProperty("java.class.path");
if(classPath != null && classPath.trim().length() > 0) {
cmd.add("--class-path");
cmd.add(classPath);
}
}
/**
* Adds a main class to the command list. Adds
* module/className or just className depending on whether
* the Job class was loaded in a named module or unnamed
* module
* @param cmd The command list
*/
private static void addMainClass(List<String> cmd) {
Class<Job> cls = Job.class;
String className = cls.getName();
Module module = cls.getModule();
if(module.isNamed()) {
String moduleName = module.getName();
cmd.add("--module");
cmd.add(moduleName + "/" + className);
} else {
cmd.add(className);
}
}
}
Job類包含一個啟動新進程的startProcess(long sleepInterval,long sleepDuration)方法。 它以Job類作為主類啟動一個JVM。 將睡眠間隔和持續時間作為參數傳遞給JVM。 該方法嘗試構建一個從JDK_HOME\bin目錄下啟動java的命令。 如果Job類被加載到一個命名的模塊中,它將生成一個如下命令:
JDK_HOME\bin\java --module-path <module-path> --module com.jdojo.process.api/com.jdojo.process.api.Job <sleepInterval> <sleepDuration>
如果Job類被加載到一個未命名的模塊中,它將嘗試構建如下命令:
JDK_HOME\bin\java -class-path <class-path> com.jdojo.process.api.Job <sleepInterval> <sleepDuration>
startProcess()方法打印用於啟動進程的命令,嘗試啟動進程,並返回進程引用。
addJvmPath()方法將JVM路徑添加到命令列表中。 它嘗試獲取當前JVM進程的命令作為新進程的JVM路徑。 如果它不可用,將嘗試從java.home系統屬性構建它。
Job類包含幾個實用程序方法,用於構成命令的一部分並解析參數並傳遞給main()方法。 具體請參考Javadoc的說明。
如果要啟動一個新進程,運行15秒鍾並且每5秒鍾喚醒,可以使用Job類的startProcess()方法:
// Start a process that runs for 15 seconds
Process p = Job.startProcess(5, 15);
可以使用CurrentProcessInfo類的printInfo()方法來打印進程細節:
// Get the handle of the current process
ProcessHandle handle = p.toHandle();
// Print the process details
CurrentProcessInfo.printInfo(handle);
當進程終止時,可以使用ProcessHandle的onExit()方法的返回值來運行任務。
CompletableFuture<ProcessHandle> future = handle.onExit();
// Print a message when process terminates
future.thenAccept((ProcessHandle ph) -> {
System.out.printf("Job (pid=%d) terminated.%n", ph.getPid());
});
可以等待新進程終止:
// Wait for the process to terminate
future.get();
在這個例子中,future.get()返回進程的ProcessHandle。 沒有使用返回值,因為已經在handle變量中。
下面包含了StartProcessTest類的代碼,它顯示了如何使用Job類創建一個新進程。 在main()方法中,它創建一個新進程,打印進程詳細信息,向進程添加關閉任務,等待進程終止,並再次打印進程細節。 請注意,該進程運行15秒,但它僅使用0.359375秒的CPU時間,因為大多數時間進程的主線程正在休眠。 以下輸入結果當程序在Windows 10上運行時生成輸出。
// StartProcessTest.java
package com.jdojo.process.api;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class StartProcessTest {
public static void main(String[] args) {
// Start a process that runs for 15 seconds
Process p = Job.startProcess(5, 15);
if (p == null) {
System.out.println("Could not create a new process.");
return;
}
// Get the handle of the current process
ProcessHandle handle = p.toHandle();
// Print the process details
CurrentProcessInfo.printInfo(handle);
CompletableFuture<ProcessHandle> future = handle.onExit();
// Print a message when process terminates
future.thenAccept((ProcessHandle ph) -> {
System.out.printf("Job (pid=%d) terminated.%n", ph.getPid());
});
try {
// Wait for the process to complete
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// Print process details again
CurrentProcessInfo.printInfo(handle);
}
}
輸出結果為:
C:\java9\bin\java.exe --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --class-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.Job 5 15
PID: 10928
IsAlive: true
Command: C:\java9\bin\java.exe
Arguments: []
CommandLine:
Start Time: 2016-11-28T13:43:28.318-06:00[America/Chicago]
CPU Time: PT0S
Owner: kishori\ksharan
Children Count: 1
Job (pid=10928) info: Sleep Interval=5 seconds, Sleep Duration=15 seconds.
Job (pid=10928) is going to sleep for 5 seconds.
Job (pid=10928) is going to sleep for 5 seconds.
Job (pid=10928) is going to sleep for 5 seconds.
Job (pid=10928) terminated.
PID: 10928
IsAlive: false
Command:
Arguments: []
CommandLine:
Start Time: 2016-11-28T13:43:28.318-06:00[America/Chicago]
CPU Time: PT0.359375S
Owner: kishori\ksharan
Children Count: 0
六. 獲取進程句柄
有幾種方法來獲取本地進程的句柄。 對於由Java代碼創建的進程,可以使用Process類的toHandle()方法獲取一個ProcessHandle。 本地進程也可以從JVM外部創建。 ProcessHandle接口包含以下方法來獲取本地進程的句柄:
static Optional<ProcessHandle> of(long pid)
static ProcessHandle current()
Optional<ProcessHandle> parent()
Stream<ProcessHandle> children()
Stream<ProcessHandle> descendants()
static Stream<ProcessHandle> allProcesses()
of()靜態方法返回指定pid的Optional<ProcessHandle>。 如果沒有此pid的進程,則返回一個空Optional。 要使用此方法,需要知道進程的PID:
// Get the process handle of the process with the pid of 1234
Optional<ProcessHandle> handle = ProcessHandle.of(1234L);
靜態current()方法返回當前進程的句柄,它始終是執行代碼的Java進程。
parent()方法返回父進程的句柄。 如果進程沒有父進程或父進程無法檢索,則返回一個空Optional。
children()方法返回進程的所有直接子進程的快照。 不能保證此方法返回的進程仍然存在。 請注意,一個不存在的進程沒有子進程。
descendants()方法返回直接或間接進程的所有子進程的快照。
allProcesses()方法返回對此進程可見的所有進程的快照。 不保證流在流處理時包含操作系統中的所有進程。
獲取快照后,進程可能已被終止或創建。 以下代碼段打印按其PID排序的所有進程的PID:
System.out.printf("All processes PIDs:%n");
ProcessHandle.allProcesses()
.map(ph -> ph.getPid())
.sorted()
.forEach(System.out::println);
可以為所有運行的進程計算不同類型的統計信息。 還可以在Java中創建一個任務管理器,顯示一個UI,顯示所有正在運行的進程及其屬性。 下面代碼顯示了如何獲得運行時間最長的進程細節以及最多使用CPU時間的進程。 比較了進程的開始時間,以獲得最長的運行進程和總CPU持續時間,以獲得使用CPU時間最多的進程。 你可能會得到不同的輸出。 代碼在Windows 10上運行程序時,得到了這個輸出。
// ProcessStats.java
package com.jdojo.process.api;
import java.time.Duration;
import java.time.Instant;
public class ProcessStats {
public static void main(String[] args) {
System.out.printf("Longest CPU User Process:%n");
ProcessHandle.allProcesses()
.max(ProcessStats::compareCpuTime)
.ifPresent(CurrentProcessInfo::printInfo);
System.out.printf("%nLongest Running Process:%n");
ProcessHandle.allProcesses()
.max(ProcessStats::compareStartTime)
.ifPresent(CurrentProcessInfo::printInfo);
}
public static int compareCpuTime(ProcessHandle ph1,
ProcessHandle ph2) {
return ph1.info()
.totalCpuDuration()
.orElse(Duration.ZERO)
.compareTo(ph2.info()
.totalCpuDuration()
.orElse(Duration.ZERO));
}
public static int compareStartTime(ProcessHandle ph1,
ProcessHandle ph2) {
return ph1.info()
.startInstant()
.orElse(Instant.now())
.compareTo(ph2.info()
.startInstant()
.orElse(Instant.now()));
}
}
輸出結果為:
Longest CPU User Process:
PID: 10696
IsAlive: true
Command: C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
Arguments: []
CommandLine:
Start Time: 2016-11-28T10:12:08.537-06:00[America/Chicago]
CPU Time: PT14M26.5S
Owner: kishori\ksharan
Children Count: 0
Longest Running Process:
PID: 0
IsAlive: false
Command:
Arguments: []
CommandLine:
Start Time: 2016-11-29T13:18:22.262776600-06:00[America/Chicago]
CPU Time: PT0S
Owner: Unknown
Children Count: 127
七. 終止進程
可以使用ProcessHandle接口和Process類的destroy()或destroyForcibly()方法終止進程。 如果終止進程的請求成功,則兩個方法都返回true,否則返回false。 destroy()方法請求正常終止,而destroyForcibly()方法請求強制終止。 在執行終止進程的請求后,isAlive()方法可以在短時間內返回true。
Tips
無法終止當前進程。 調用當前進程中的destroy()或destroyForcibly()方法會引發IllegalStateException異常。 操作系統訪問控制可能會阻止進程終止。
一個進程的正常終止讓進程徹底終止。 強制終止流程將立即終止流程。 進程是否正常終止是依賴於實現的。 可以使用ProcessHandle接口的supportsNormalTermination()方法和Process類來檢查進程是否支持正常終止。 如果進程支持正常終止,該方法返回true,否則返回false。
調用這些方法來終止已經被終止的進程導致沒有任何操作。 當進程結束后,Process類的onExit()返CompletableFuture<Process>,ProcessHandle接口的onExit()方法返回CompletableFuture<ProcessHandle>。
八. 管理進程權限
運行上一節中的示例時,認為沒有安裝Java安全管理器。 如果安裝了安全管理器,則需要授予適當的權限才能啟動,管理和查詢本地進程:
- 如果要創建新進程,則需要具有
FilePermission(cmd,"execute")權限,其中cmd是將創建進程的命令的絕對路徑。 如果cmd不是絕對路徑,則需要具有FilePermission("<<ALL FILES>>","execute")權限。 - 使用
ProcessHandle接口中的方法來查詢本地進程的狀態並銷毀進程,應用程序需要具有RuntimePermission("manageProcess")權限。
下面包含一個獲取進程計數並創建新進程的程序。 它重復這兩個任務,一個任務沒有安全管理員權限,而另一個任務有安全管理員權限。
// ManageProcessPermission.java
package com.jdojo.process.api;
import java.util.concurrent.ExecutionException;
public class ManageProcessPermission {
public static void main(String[] args) {
// Get the process count
long count = ProcessHandle.allProcesses().count();
System.out.printf("Process Count: %d%n", count);
// Start a new process
Process p = Job.startProcess(1, 3);
try {
p.toHandle().onExit().get();
} catch (InterruptedException | ExecutionException e) {
System.out.println(e.getMessage());
}
// Install a security manager
SecurityManager sm = System.getSecurityManager();
if(sm == null) {
System.setSecurityManager(new SecurityManager());
System.out.println("A security manager is installed.");
}
// Get the process count
try {
count = ProcessHandle.allProcesses().count();
System.out.printf("Process Count: %d%n", count);
} catch(RuntimeException e) {
System.out.println("Could not get a " +
"process count: " + e.getMessage());
}
// Start a new process
try {
p = Job.startProcess(1, 3);
p.toHandle().onExit().get();
} catch (InterruptedException | ExecutionException |
RuntimeException e) {
System.out.println("Could not start a new " +
"process: " + e.getMessage());
}
}
}
假設沒有更改任何Java策略文件,請嘗試使用以下命令運行ManageProcessPermission類:
C:\Java9Revealed>java --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.ManageProcessPermission
輸出結果為:
Command used:
C:\java9\bin\java.exe --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.Job 1 3
Job (pid=6320) info: Sleep Interval=1 seconds, Sleep Duration=3 seconds.
Job (pid=6320) is going to sleep for 1 seconds.
Job (pid=6320) is going to sleep for 1 seconds.
Job (pid=6320) is going to sleep for 1 seconds.
A security manager is installed.
Could not get a process count: access denied ("java.lang.RuntimePermission" "manageProcess")
Could not start a new process: access denied ("java.lang.RuntimePermission" "manageProcess")
你可能會得到不同的輸出。 輸出表示可以在安裝安全管理器之前獲取進程計數並創建新進程。 安裝安全管理器后,Java運行時會在請求進程計數和創建新進程時拋出異常。 要解決此問題,需要授予以下四個權限:
- “manageProcess” 運行時權限,它將允許應用程序查詢本地進程並創建一個新進程。
- 在Java命令路徑上“execute” 文件權限,這將允許啟動JVM。
- 在系統屬性“jdk.module.path”和“java.class.path”中“read”的屬性權限,因此在創建命令行以啟動JVM時,
Job類可以讀取這些屬性。
如下包含一個腳本,將這四個權限授予所有代碼。 需要將此腳本添加到計算機上的JDK_HOME\conf\security\java.policy文件中。 Java啟動器的路徑是C:\java9\bin\java.exe,只有在C:\java9目錄中安裝了JDK 9,才在Windows上有效。 對於所有其他平台和JDK安裝,請修改此路徑以指向計算機上正確的Java啟動器。
grant {
permission java.lang.RuntimePermission "manageProcess";
permission java.io.FilePermission "C:\\java9\\bin\\java.exe", "execute";
permission java.util.PropertyPermission "jdk.module.path", "read";
permission java.util.PropertyPermission "java.class.path", "read";
};
如果使用相同的命令再次運行ManageProcessPermission類,則應該獲得類似於以下內容的輸出:
Process Count: 133
Command used:
C:\java9\bin\java.exe --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.Job 1 3
Job (pid=3108) info: Sleep Interval=1 seconds, Sleep Duration=3 seconds.
Job (pid=3108) is going to sleep for 1 seconds.
Job (pid=3108) is going to sleep for 1 seconds.
Job (pid=3108) is going to sleep for 1 seconds.
A security manager is installed.
Process Count: 133
Command used:
C:\java9\bin\java.exe --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.Job 1 3
Job (pid=3684) info: Sleep Interval=1 seconds, Sleep Duration=3 seconds.
Job (pid=3684) is going to sleep for 1 seconds.
Job (pid=3684) is going to sleep for 1 seconds .
Job (pid=3684) is going to sleep for 1 seconds.
九. 總結
Process API由使用本地進程的類和接口組成。 Java SE從版本1.0通過運行時和進程類提供了Process API。 它允許創建新的本地進程,管理其I/O流並銷毀它們。 Java SE的更新版本改進了API。 直到Java 9,開發人員必須訴諸編寫本地代碼來獲取基本信息,例如進程的ID,用於啟動進程的命令等。Java 9添加了一個名為ProcessHandle的接口,表示進程句柄。 可以使用進程句柄來查詢和管理本地進程。
以下類和接口組成了Process API:Runtime,ProcessBuilder,ProcessBuilder.Redirect,Process,ProcessHandle和ProcessHandle.Info。
Runtime類的exec()方法用於啟動本地進程。
ProcessBuilder類的start()方法是優先於Runtime類的exec()方法來啟動進程。 ProcessBuilder.Redirect類的實例表示進程的進程輸入源或進程的目標輸出。
Process類的實例表示由Java程序創建的本地進程。
ProcessHandle接口的實例表示由Java程序或其他方式創建的進程。它在Java 9中添加,並提供了幾種方法來查詢和管理進程。 ProcessHandle.Info接口的實例表示進程的快照信息; 它可以使用Process類或ProcessHandle接口的info()方法獲得。 如果有一個進程實例,使用它的toHandle()方法獲得一個ProcessHandle。
ProcessHandle接口的onExit()方法返回一個用於終止進程的CompletableFuture<ProcessHandle>。 可以使用返回的對象來添加在進程終止時執行的任務。 請注意,不能在當前進程中使用此方法。
如果安裝了一個安全管理器,則應用程序需要有一個“manageProcess”運行時權限來查詢和管理本地進程,並在Java代碼啟動的進程的命令文件上“execute” 文件權限。
