从哪个 Linux 内核/libc 版本开始,Java Runtime.exec() 在内存方面是安全的?

StackOverflow https://stackoverflow.com/questions/209875

在工作中,我们的目标平台之一是运行 Linux 的资源受限的迷你服务器(内核 2.6.13,基于旧 Fedora Core 的自定义发行版)。该应用程序是用 Java (Sun JDK 1.6_04) 编写的。Linux OOM Killer 配置为在内存使用量超过 160MB 时终止进程。即使在高负载期间,我们的应用程序也不会超过 120MB,并且与其他一些处于活动状态的本机进程一起,我们仍保持在 OOM 限制之内。

然而,事实证明,Java Runtime.getRuntime().exec() 方法(从 Java 执行外部进程的规范方法)有一个 Linux 上特别不幸的实现 由于复制了地址空间,这会导致生成的子进程(暂时)需要与父进程相同的内存量。最终结果是,一旦我们执行 Runtime.getRuntime().exec(),我们的应用程序就会被 OOM 杀手杀死。

目前,我们通过让一个单独的本机程序执行所有外部命令并通过套接字与该程序进行通信来解决此问题。这不是最佳的。

在线发布有关此问题的帖子 我收到一些反馈,表明这种情况不应发生在“较新”版本的 Linux 上,因为它们使用写时复制实现 posix fork() 方法,大概意味着它只会在需要时复制需要修改的页面立即整个地址空间。

我的问题是:

  • 这是真的?
  • 这是内核、libc 实现还是其他地方的东西?
  • 从哪个版本的 kernel/libc/whatever 可以使用 fork() 的写时复制功能?
有帮助吗?

解决方案

这几乎就是 *nix(和 linux)自诞生以来(或者至少是 mmus 诞生以来)的工作方式。

要在 *nix 上创建新进程,请调用 fork()。fork() 创建调用进程的副本及其所有内存映射、文件描述符等。内存映射是写时复制完成的,因此(在最佳情况下)实际上不会复制内存,仅复制映射。接下来的 exec() 调用将当前内存映射替换为新可执行文件的内存映射。因此,fork()/exec() 是创建新进程的方式,这也是 JVM 使用的方式。

需要注意的是,对于繁忙系统上的巨大进程,父进程可能会继续运行一段时间,然后子进程 exec() 会因写入时复制而导致复制大量内存。在虚拟机中,内存可以大量移动,以方便垃圾收集器产生更多的复制。

“解决方法”是做你已经做过的事情,创建一个外部轻量级进程来负责生成新进程 - 或者使用比 fork/exec 更轻量级的方法来生成进程(Linux 没有 - 并且无论如何都会这样做)需要更改 jvm 本身)。Posix 指定了 posix_spawn() 函数,理论上可以在不复制调用进程的内存映射的情况下实现 - 但在 Linux 上则不然。

其他提示

好吧,我个人怀疑这是真的,因为 Linux 的 fork() 是通过写时复制完成的,因为天知道什么时候(至少 2.2.x 内核有它,它是在 199x 的某个地方)。

由于 OOM Killer 被认为是一个相当粗糙的工具,已知会失败(例如,它不必杀死实际分配大部分内存的进程)并且应该仅用作最后的报告,因此尚不清楚我为什么你把它配置为在 160M 上启动。

如果你想对内存分配施加限制,那么 ulimit 是你的朋友,而不是 OOM。

我的建议是不要管 OOM(或完全禁用它),配置 ulimit,然后忘记这个问题。

是的,即使是新版本的 Linux(我们使用的是 64 位 Red Hat 5.2),情况也是如此。大约 18 个月以来,我一直遇到子进程运行缓慢的问题,直到我阅读您的问题并运行测试来验证它之前,我永远无法找出问题所在。

我们有一个具有 16 个核心的 32 GB 机器,如果我们使用 -Xms4g 和 -Xmx8g 等设置运行 JVM,并使用 Runtime.exec() 和 16 个线程运行子进程,我们的进程运行速度将无法超过大约 20每秒处理调用次数。

在 Linux 中使用简单的“date”命令尝试大约 10,000 次。如果您添加分析代码来观察正在发生的情况,它会很快启动,但随着时间的推移会减慢。

阅读您的问题后,我决定尝试将内存设置降低到 -Xms128m 和 -Xmx128m。现在我们的进程以每秒大约 80 次进程调用的速度运行。我只更改了 JVM 内存设置。

即使我尝试使用 32 个线程,它似乎也不会以这样的方式耗尽内存。只是必须以某种方式分配额外的内存,这会导致沉重的启动(可能还有关闭)成本。

不管怎样,似乎应该有一个设置来禁用 Linux 甚至 JVM 中的这种行为。

1:是的。2:这分为两个步骤:任何像 fork() 这样的系统调用都会被 glibc 包装到内核中。系统通话的内核部分位于内核/fork.c 3:我不知道。但我敢打赌你的内核有它。

当 32 位机器上内存不足时,OOM 杀手就会启动。我从来没有遇到过这个问题,但是有一些方法可以避免 OOM。此问题可能是某些 OOM 配置问题。

由于您使用的是 Java 应用程序,因此您应该考虑迁移到 64 位 Linux。那绝对应该解决它。只要安装了相关库,大多数 32 位应用程序都可以在 64 位内核上运行,不会出现任何问题。

您还可以尝试 32 位 fedora 的 PAE 内核。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top