Java:流资源的使用和理解

郎家岭伯爵 2026年04月27日 5次浏览

前言

最近在做一个需求时,需要在 Linux 服务器上获取某个磁盘上挂载点的大小,写了一段逻辑但是发现有些问题(没有显式的关闭资源)。为了排查是否会造成影响,仔细分析了下这段代码,也加深了对 Java 中流资源的理解。这里记录下。

实现

代码

以下是两段代码,第一段是问题代码,第二段是修复后的代码。可以看到两者的区别在于是否显式地关闭了资源

针对 InputStream 流,使用 try-with-resources 语法来关闭资源;针对 Process 资源,在 finally 语法块中显示地调用 process.destroy() 关闭资源。

问题代码

private static String getDiskUsageDetail1(String diskName) throws IOException, InterruptedException {
	ProcessBuilder processBuilder = new ProcessBuilder("df", "--block-size=1M", diskName);
	Process start = processBuilder.start();
	BufferedReader br = new BufferedReader(new InputStreamReader(start.getInputStream()));
	// 去掉表头
	br.readLine();
	String s = br.readLine();
	start.waitFor();
	return s.split("\\s+")[2];
}

修复后的代码

private static String getDiskUsageDetail(String diskName) throws IOException, InterruptedException {
	ProcessBuilder processBuilder = new ProcessBuilder("df", "--block-size=1M", diskName);
	Process process = processBuilder.start();

	try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
		// 跳过表头
		br.readLine();
		String line = br.readLine();
		// 等待进程执行完毕,获取返回值
		int code = process.waitFor();
		return line.split("\\s+")[2];
	} finally {
		// 显式销毁进程,释放相关流资源
		process.destroy();
	}
}

分析

Stream流是什么?

Stream 流可以形象地理解为一个“水管”。需要注意这里还没有读取实际内容,也就是没有“水”。

以上面的代码为例:

当执行 df 命令时,数据并不是直接飞进 String line 里的。它经历了一个类似“水箱传水”的过程:

  1. 内核缓冲区(水库): df 命令执行后,Linux 内核会开辟一块内存区域(Pipe Buffer)。df 把结果写进去。此时,数据在内核手里。
  2. Java 输入流(抽水机): process.getInputStream() 就像连接到水库的管子。
  3. readLine() 命令(抽水动作): 关键点就在这里! 只有执行 br.readLine() 时,Java 才会向内核发起系统调用,把数据从内核的“水库”搬运到 Java 进程的内存里。

如果不调用 read 或 readLine,数据就会一直堆积在内核的缓冲区里,哪儿也不去。

如果代码只写了 start() 而没有写 br.readLine()
• 内核缓冲区会被填满。
• 一旦填满(通常是 64KB),df 命令就会被内核“挂起”(暂停执行),因为它没地方写了。
• Java 程序如果不去“抽水”,这些数据永远不会出现在 Java 内存中。

缓冲区写满会怎么样?

上一小节中提到 Java 需要通过 readLine() 方法去“抽水”,如果没有 readLine() 去“抽水”,把缓冲区写满会发生什么呢?

是仅输出存在阻塞还是整个进程都阻塞?

  • 整个进程被操作系统挂起,不再占用 CPU 资源。

可能造成的严重问题?

  • 可能会造成死锁,进程永远无法完成。

如何处理缓冲区中的内容?

简单法则:要么吃掉输出,要么倒掉输出,但绝不能放着不管!

吃掉输出

使用 readLine() 去读取是最常用的方式,这里获取的内容可以不处理。

示例代码段:

while ((line = reader.readLine()) != null) {
	// 可以选择处理或直接丢弃
	// 这里只是读取,防止缓冲区满
}

注:

  • “吃掉输出”相对来讲较为复杂,当服务器负载较高的时候,很可能会出现消耗的速度比产生的速度慢,这样同样存在写满缓冲区的风险。

倒掉输出

重定向到 DISCARD。

示例代码段:

// 方法1:重定向到 DISCARD(完全丢弃)
ProcessBuilder pb = new ProcessBuilder("your-command");

// 重定向输出到黑洞
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
pb.redirectError(ProcessBuilder.Redirect.DISCARD);

Process process = pb.start();
int exitCode = process.waitFor();  // 不会阻塞

注:

  • “倒掉输出”是一种处理逻辑,并不一定要在 Java 代码中处理,也可以在 Shell 命令中处理,例如使用 > 重定向到指定的文件中。

process.waitFor()应该放在哪里?

process.waitFor() 是获取命令的执行结果。那么它应该在 readLine() 之前还是之后执行呢?

前面已经写过,readLine() 方法是读取输出防止缓冲区写满的。此时存在两种情况:

  1. 输出很多。此时应该先 readLine(),则应该把 waitFor() 方法放在后面(但需要注意由于输出读取和产生的速度不一致,依然有写满缓冲区的风险)。
  2. 输出少于64K。此时无需关注两者的先后顺序,因为永远不会写满缓冲区。

其它的特殊情形

很多时候我们在实现功能时,都需要结合实际场景去考虑。例如上面的“问题代码”,也不见得一定会出现问题。在服务器负载很低时,Java 方法很快会执行结束,而且 process 构建的 Shell 命令也很简单,也会很快结束。在并发较低时,JVM 发生 GC 时会帮我们回收资源,同时服务器资源也基本不会有消耗。

上述这种场景,就不会出现问题。

当然了,问题代码还是要修复,但结合实际情况分析问题更重要

句柄/文件描述符

FD 的全称是 File Descriptor,在 Windows 系统中称为“句柄”,在 Linux 系统中称为“文件描述符”。

在 Linux 中,FD 不仅仅代表硬盘上的文件。以下资源在 Linux 眼里都是“文件”,都会占用 FD:

  1. 普通文件(磁盘上的 .txt, .log 等)。
  2. 网络套接字 (Sockets)(每一个 TCP 连接都是一个 FD)。
  3. 管道 (Pipes):这就是你代码中产生 FD 的原因。当你启动 df 命令,Java 需要通过一个管道来读取 df 的输出,这个管道的“读端”就是一个 FD。
  4. 标准输入/输出:每个进程默认占用 3 个 FD:
    • 0: 标准输入 (stdin)
    • 1: 标准输出 (stdout)
    • 2: 标准错误 (stderr)

Linux 系统中,给每个进程分配的默认 FD 个数为 1024,超过就会报错,例如我们常见的 Too many open files。但例如我们在 Linux 系统中启动了两个 Java 服务 A 和 B,如果服务 A 超出上限,并不会影响服务 B。每个进程是独享 1024 个 FD 的。

总结

记录下 Java 中流资源的知识点,加深理解。