23

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

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

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

网上发布这个问题后,我得到了一些反馈,表明这不应该发生在“较新”版本的 Linux 上,因为它们使用写时复制实现了 posix fork() 方法,大概意味着它只会复制它需要的页面在需要时修改而不是立即修改整个地址空间。

我的问题是:

  • 这是真的?
  • 这是内核、libc 实现还是其他地方的东西?
  • 从哪个版本的内核/libc/fork() 可用的写时复制?
4

4 回答 4

11

这几乎是 *nix(和 linux)从时间之初(或至少在 mmus 之初)开始工作的方式。

要在 *nixes 上创建一个新进程,请调用 fork()。fork() 使用其所有内存映射、文件描述符等创建调用进程的副本。内存映射是在写入时复制完成的,因此(在最佳情况下)实际上没有复制内存,只有映射。随后的 exec() 调用将当前内存映射替换为新可执行文件的映射。因此,fork()/exec() 是您创建新进程的方式,这就是 JVM 使用的方式。

需要注意的是,在繁忙的系统上有大量进程,在子 exec() 导致大量内存被复制之前,父进程可能会继续运行一段时间,因为写入时复制。在虚拟机中,内存可以移动很多,以促进垃圾收集器产生更多的复制。

The "workaround" is to do what you've already done, create an external lightweight process that takes care of spawning new processes - or use a more lightweight approach than fork/exec to spawn processes (Which linux does not have - and would anyway require a change in the jvm itself). Posix specifies the posix_spawn() function, which in theory can be implemented without copying the memory mapping of the calling process - but on linux it isn't.

于 2009-12-16T14:25:43.813 回答
5

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

由于 OOM 杀手被认为是一种相当粗糙的工具,已知会失火(fe,它没有必要杀死实际分配大部分内存的进程)并且应该仅用作最后的报告,因此尚不清楚我为什么将其配置为在 160M 上触发。

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

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

于 2008-10-16T22:05:40.177 回答
2

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

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

在 Linux 中使用简单的“date”命令尝试此操作大约 10,000 次。如果你添加分析代码来观察正在发生的事情,它会很快开始,但随着时间的推移会变慢。

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

即使我用 32 个线程尝试过它,它似乎也没有以我曾经内存不足的方式吸收内存。只是必须以某种方式分配额外的内存,这会导致沉重的启动(甚至可能是关闭)成本。

无论如何,似乎应该有一个设置来禁用这种行为 Linux 甚至在 JVM 中。

于 2009-12-16T14:07:18.340 回答
1

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

当 32 位机器上的低内存受到威胁时,OOM 杀手就会发挥作用。我从来没有遇到过这个问题,但是有一些方法可以阻止 OOM。这个问题可能是一些 OOM 配置问题。

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

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

于 2008-10-16T21:02:01.537 回答