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

23

在我们的工作中,一个目标平台是运行Linux的资源受限小型服务器(内核2.6.13,基于旧版Fedora Core的自定义发行版)。该应用程序使用Java编写(Sun JDK 1.6_04)。当内存使用量超过160MB时,Linux OOM killer被配置为杀死进程。即使在高负载下,我们的应用程序也从未超过120MB,加上一些其他活动的本地进程,我们仍然远低于OOM限制。

然而,事实证明,Java Runtime.getRuntime().exec()方法(从Java执行外部进程的标准方式)在Linux上具有特别不幸的实现,因为生成的子进程会(暂时)需要与父进程相同数量的内存,因为地址空间被复制。结果是,我们的应用程序在进行Runtime.getRuntime().exec()操作后很快就被OOM killer杀死了。

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

在线发布此问题后,我得到了一些反馈,表明在“更新”的Linux版本上不应该发生这种情况,因为它们使用copy-on-write实现posix fork() 方法,可能意味着只有在需要修改时才会立即复制整个地址空间的页面。

我的问题是:

  • 这是真的吗?
  • 这是内核、libc实现或完全其他地方的东西吗?
  • 从哪个内核/libc/任何东西的版本开始可用fork()的copy-on-write?

在fork()调用和接下来的exec()之间,需要的是虚拟内存而不是物理内存。考虑到地址空间的大小与物理内存限制相比,我非常怀疑你是否会耗尽虚拟内存。 - Charles Duffy
确实,我们的物理内存并没有耗尽,但是 Linux 的某些部分似乎认为我们已经用尽了。 - Boris Terzic
4个回答

11

自从Unix(和Linux)诞生以来,就一直采用这种方式。

要在Unix上创建一个新进程,需要调用fork()。fork()会创建一个调用进程的副本,包括其所有的内存映射、文件描述符等。内存映射是通过写时复制来完成的,因此(在最优的情况下)实际上没有复制任何内存,只有映射。接下来的exec()调用将当前的内存映射替换为新可执行文件的映射。因此,fork()/exec()是创建新进程的方式,也是JVM使用的方式。

不过,在繁忙系统上处理巨大进程时,父进程可能会在子进程执行exec()之前继续运行一段时间,导致由于写时复制而复制大量内存。在虚拟机中,为了方便垃圾收集器移动内存,内存可以被频繁地移动,从而产生更多的复制。

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


1
我会称呼这个轻量级进程为ssh,并使用http://www.jcraft.com/jsch/连接到本地主机。 - JoG

5

嗯,我个人怀疑这是真的,因为自从神知道什么时候开始(至少2.2.x内核有它,并且在199x年间某个时间点)Linux的fork()是通过写时复制完成的。

由于OOM killer被认为是一个相当粗糙的工具,已知会误杀进程(例如,它不一定杀死实际分配大部分内存的进程),并且应该仅作为最后的手段使用,所以对我来说不清楚为什么你配置它在160M时触发。

如果您想对内存分配设置限制,则ulimit是一个好帮手,而不是OOM。

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


感谢提供信息,遗憾的是我们无法控制平台:硬件、操作系统和配置大多已经固定(我们只是在上面部署)。 - Boris Terzic

2
是的,即使是新版本的Linux(我们正在使用64位Red Hat 5.2),也存在这种情况。我遇到了一个慢速运行子进程的问题已有18个月之久,一直无法找出问题所在,直到看到您的问题并进行测试验证后才得以解决。
我们拥有一个32GB内存和16核心的服务器,如果我们使用-Xms4g和-Xmx8g等设置运行JVM,并使用Runtime.exec()函数在16个线程中运行子进程,则每秒最多只能运行大约20个进程调用。
请尝试在Linux上运行简单的“date”命令大约10,000次。如果添加分析代码来观察发生了什么,它会快速启动,但随着时间的推移会变慢。
在阅读了您的问题后,我决定将内存设置降低到-Xms128m和-Xmx128m。现在,我们的进程每秒运行约80个进程调用。我所做的只是更改了JVM内存设置。
即使我尝试使用32个线程,它似乎也没有吸收那么多内存,我从未耗尽内存。只是需要以某种方式分配额外的内存,这会导致较高的启动(和关闭)成本。
总之,似乎应该有一种设置可以禁用Linux或甚至JVM中的此行为。

1

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

当32位系统的低内存受到威胁时,OOM killer会启动。我从未遇到过这个问题,但有方法可以防止OOM。这个问题可能是一些OOM配置问题。

由于您正在使用Java应用程序,因此应考虑迁移到64位Linux。这肯定会解决问题。只要安装了相关库,大多数32位应用程序都可以在64位内核上运行而没有问题。

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


为什么转换到64位JVM会解决这个问题? - Alastair McCormack
1
32位Linux系统将用户空间和内核使用了3GB/1GB的分割。其中1GB的一部分被保留给需要通过系统调用访问内核空间的用户空间进程,仅为128M。我认为在128M内核和3GB用户空间中的内存压力都会触发OOM。虽然可能有其他进程在3GB空间中,但JVM更有可能成为最大的进程,因此成为OOM的目标。此外,像OP的JVM这样的分叉进程具有更高的OOM分数,因为每个子进程都会增加父进程的分数。64位没有任何内存分割或任何内核/用户分割。 - Bash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接