java启动进程的实现过程

 更新时间:2026年06月17日 11:32:13   作者:las_learn  
这段描述主要介绍了Java中启动进程的两种方式:`Runtime.exec`和`ProcessBuilder`,并详细解析了`ProcessBuilder`的核心参数及其使用注意事项,特别强调了进程流交互时的管道大小问题及解决策略

或多或少我们需要在java 中启动一个单独进程,去获取命令行的输出/异常输出。 我们自然会产生如下几个问题

  • java为我们提供了哪些api,可以实现进程启动?
  • 进程启动的一些参数我们是否可以配置?
  • 使用时我们需要注意什么

执行命令启动进程的两种方式

实例一: 使用Runtime.exec 执行echo dial

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class RuntimeExec {
    public static void main(String[] args) throws IOException, InterruptedException {
        Process process = Runtime.getRuntime().exec(new String[]{"echo", "dial"});
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))){
            System.out.println("current -----------");
            bufferedReader.lines().forEach(line -> {
                System.out.println(line);
            });
        }
        process.waitFor();
        System.out.println(String.format("process exit value is :%d", process.exitValue()));
    }
}

实例二: 使用ProcessBuilder 执行echo dial

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ProcessBuilderTest {
    public static void main(String[] args) throws IOException, InterruptedException {
        ProcessBuilder processBuilder = new ProcessBuilder();
        processBuilder.command(new String[]{"echo", "dial"});
        Process process = processBuilder.start();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))){
            System.out.println("current -----------");
            bufferedReader.lines().forEach(line -> {
                System.out.println(line);
            });
        }
        process.waitFor();
        System.out.println(String.format("process exit value is :%d", process.exitValue()));
    }
}

启动命令的分析

我们去看下Runtime的exec函数的实现,其实就是调用ProcessBuilder的方法启动一个进程,具体如下:

public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

我们来分析进程启动的具体的流程就只需要查看ProcessBuilder相关的功能即可。从名字上可以看出这个类主要就使用构造器的模式创建出一个Process。

ProcessBuilder的参数

查看ProcessBuilder的实现中,我们可以发现主要就包含如下几个核心参数:

  • command, 命令及其参数列表
  • directory, 进程的工作目录
  • environment,进程的环境变量
  • redirectErrorStream,是否重定向异常流,该参数主要用于是否将异常流重定向到标准输出流中
  • redirects, 进程的标准流信息。 一个进程至少有三个标准流,stdin,stdout,stderr。此处的redirects代表的就是三个标准流

ProcessBuilder的函数

command/directory两类函数就是设置相应的参数,没有过多的逻辑,就不做进一步的描述

environment(String[] envp)

该函数的访问权限是包内访问的权限,不对外提供,默认情况下就是使用java启动获取到的信息,使用Runtime的方式可以调整默认的环境变量信息。

我们在linux中可以使用env 打印出当前bash的环境变量,那么我们在当前bash启动java程序的时候会可以获取到本地的环境变量,例如我们想获取到启动java环境变量中的SHELL信息,System.getEnv(“SHEEL”)即可。环境变量是一个key value的信息,该函数的主要实现也就是将String[] 转化为一个Map<String, String>信息

ProcessBuilder environment(String[] envp) {
    assert environment == null;
    if (envp != null) {
        environment = ProcessEnvironment.emptyEnvironment(envp.length);
        for (String envstring : envp) {
            if (envstring.indexOf((int) '\u0000') != -1)
                envstring = envstring.replaceFirst("\u0000.*", "");
            int eqlsign =
                envstring.indexOf('=', ProcessEnvironment.MIN_NAME_LENGTH);
            if (eqlsign != -1)
                environment.put(envstring.substring(0,eqlsign),
                                envstring.substring(eqlsign+1));
        }
    }
    return this;
}

上面的实现很简单,如果envp不为空,则新建一个空map,将envp中的每一项按照=切割为key,value,不符合该结构的不使用。

redirectInput/redirectOutput/redirectError

前面我们已经提过了一个进程至少有三个标准流,这三个函数作用就是用于配置调整默认的三个流。 这三个流在java中默认使用的是PIPE形式,也就是通过PIPE实现java与其子进程之间的交互。通过这三个函数可以调整起默认行为。

java中的Redirect提供如下几种:

  • PIPE, java中默认的方式,如果不配置就是使用这种方式
  • INHERIT, 子进程的流使用java进程的流
  • READ, 以文件流的形式作为流
  • WRITE 以文件的形式作为流
  • APPEND 以追加的方式作为流

start

构造模式最后会生成对应的实例,通过调用前面几个函数调整默认的行为后,需要通过调用start来最终创建一个进程。 最终的实现是调用ProcessImpl的start来启动。

简化逻辑如下:

  1. 检测命令行及参数是否存在为null
  2. 检查命令行及参数是否包含\u0000字符
  3. 将命令行及参数,环境变量,工作目录,标准流,以及重定向异常流作为参数调用ProcessImpl的实现
public Process start() throws IOException {
    String[] cmdarray = command.toArray(new String[command.size()]);
    cmdarray = cmdarray.clone();
    for (String arg : cmdarray)
        if (arg == null)
            throw new NullPointerException();
    String prog = cmdarray[0];
    SecurityManager security = System.getSecurityManager();
    if (security != null)
        security.checkExec(prog);
    String dir = directory == null ? null : directory.toString();
    for (int i = 1; i < cmdarray.length; i++) {
        if (cmdarray[i].indexOf('\u0000') >= 0) {
            throw new IOException("invalid null character in command");
        }
    }
    return ProcessImpl.start(cmdarray, environment, dir, redirects,
         redirectErrorStream);
}

就不做进一步展开的说明ProcessImpl的实现,有兴趣的可以自行阅读,我们就额外描述下ProcessImpl的start函数对于redirects中的进程输入流说明一下,简化代码如下:

int[] std_fds;
FileInputStream  f0 = null;
std_fds = new int[3];
if (redirects[0] == Redirect.PIPE)
    std_fds[0] = -1;
else if (redirects[0] == Redirect.INHERIT)
    std_fds[0] = 0;
else {
    f0 = new FileInputStream(redirects[0].file());
    std_fds[0] = fdAccess.get(f0.getFD());
}

主要就分三种情况:

1、redirects[0] 为PIPE 类型的时候,将std_fds[0] 设置为-1, 设置为-1的原因是jdk内部判断到是-1情况下会自行创建一个pipe

2、redirects[0] 为INHERIT 类型的时候,将std_fds[0] 设置为0, 设置为0的原因是为0的fd代表的是当前java进程的标准输入流

3、其他情况,代表我们配置专门的文件作为输入流,就通过FileInputStream获取到对应的文件描述符

使用时我们需要注意什么

参数要求

1、传入的命令行及参数列表中不能包含null

2、传入的命令行及参数列表中不能包含\u0000 字符

3、传入的环境变量信息的格式是每一项item,以=分割为key/value两项

流的使用

我们来考虑个问题,默认情况下java与子进程的流交互使用的是PIPE进行交互,PIPE的大小不可能是无限大的,所以如果进程的输出流很大,java进程一直没有读取子进程的输出流的时候子进程就没办法继续写入数据进而会导致子进程阻塞。

如果我们这么去写代码,/Users/allen/a.out 这个是编译出的会输出65537个字符的进程,去执行的时候java进程没有办法执行(当前系统的pipe调用返回的是cache是65536的情况)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class ProcessBuilderTest {
    public static void main(String[] args) throws IOException, InterruptedException {
        Process process = Runtime.getRuntime().exec("/Users/allen/a.out", new String[]{"PWD=/"});
        process.waitFor();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))){
            System.out.println("current -----------");
            bufferedReader.lines().forEach(line -> {
                System.out.println(line);
            });
        }
        process.waitFor();
        System.out.println(String.format("process exit value is :%d", process.exitValue()));
    }
}

pipe的大小

  • 类unix, 使用pipe的系统调用实现,取决于系统的参数,默认情况下是65536
  • windows,是4096 + 24的大小

我们如何来避免这种情况

1、如果明确输出/异常信息不回达到pipe的上限我们就不用处理

2、如果我们不关注异常流,我们可以把异常流重定向到/dev/null, 在调用waitFor前去读去输出流

processBuilder.redirectError(new File("/dev/null"))

3、如果我们关注异常流和输出流,但不区分两者,我们可以将异常流重定向到输出流, 在调用waitFor前去启动线程读去输出流

processBuilder.redirectErrorStream(true)

3、如果同时关注异常流和输出流,且需要区分,在调用waitFor前去,启动新线程分别读去异常流和输出流或者使用重定向输出流/异常流到文件的方式。 waitFor结束后从文件中读取信息出来。

processBuilder.redirectError(new File('xxxx'))
processBuilder.redirectError(new File('xxxx2'))

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • SpringMVC路径规则以及使用正则详解

    SpringMVC路径规则以及使用正则详解

    本文介绍了@RequestMapping路径通配符的用法,包括*和**的区别及其灵活位置,说明了通配符与路径变量可共用,并讲解了匹配优先级规则:路径越具体优先级越高,变量比通配符更精确
    2025-10-10
  • Java中==运算符与equals方法的区别及intern方法详解

    Java中==运算符与equals方法的区别及intern方法详解

    这篇文章主要介绍了Java中==运算符与equals方法的区别及intern方法详解的相关资料,需要的朋友可以参考下
    2017-04-04
  • 关于Java创建线程的2种方式以及对比

    关于Java创建线程的2种方式以及对比

    这篇文章主要给大家介绍了关于Java创建线程的2种方式以及对比的相关资料,文中通过实例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2022-01-01
  • springboot多文件上传实现使用postman测试多文件上传接口

    springboot多文件上传实现使用postman测试多文件上传接口

    这篇文章主要介绍了springboot多文件上传实现使用postman测试多文件上传接口,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • Spring bean的实例化和IOC依赖注入详解

    Spring bean的实例化和IOC依赖注入详解

    这篇文章主要介绍了Spring bean的实例化和IOC依赖注入详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-03-03
  • Springboot+hibernate实现简单的增删改查示例

    Springboot+hibernate实现简单的增删改查示例

    今天小编就为大家分享一篇Springboot+hibernate实现简单的增删改查示例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-08-08
  • spring注解在自定义jar包中无法被扫描的解决方案

    spring注解在自定义jar包中无法被扫描的解决方案

    这篇文章主要介绍了spring注解在自定义jar包中无法被扫描的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • 选择Spring Boot项目的内嵌容器的理由

    选择Spring Boot项目的内嵌容器的理由

    Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。这篇文章主要介绍了选择Spring Boot项目的内嵌容器,需要的朋友可以参考下
    2017-11-11
  • java实现屏幕共享功能实例分析

    java实现屏幕共享功能实例分析

    这篇文章主要介绍了java实现屏幕共享功能的方法,以实例形式分析了屏幕共享功能的客户端与服务端的详细实现方法,是非常具有实用价值的技巧,需要的朋友可以参考下
    2014-12-12
  • java实现摄像头截图功能

    java实现摄像头截图功能

    这篇文章主要为大家详细介绍了java实现摄像头截图功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-10-10

最新评论