Java 9 揭秘(12. Process API 更新)


Tips
做一個終身學習的人。

Java 9

在本章中,主要介紹以下內容:

  • 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中有一些改進,就是在ProcessProcessBuilder類中添加幾個方法。

在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類包含一個返回ProcessHandletoHandle()方法。

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() 返回進程的用戶。

現在是時候看到ProcessHandleProcessHandle.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:RuntimeProcessBuilderProcessBuilder.RedirectProcessProcessHandleProcessHandle.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” 文件權限。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM