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” 文件權限。