从哪个 Linux 内核/libc 版本开始,Java Runtime.exec() 就内存而言是安全的?
Posted
技术标签:
【中文标题】从哪个 Linux 内核/libc 版本开始,Java Runtime.exec() 就内存而言是安全的?【英文标题】:From what Linux kernel/libc version is Java Runtime.exec() safe with regards to memory? 【发布时间】:2010-09-17 14:46:10 【问题描述】:在工作中,我们的目标平台之一是运行 Linux(内核 2.6.13,基于旧 Fedora Core 的自定义发行版)的资源受限的微型服务器。该应用程序是用 Java (Sun JDK 1.6_04) 编写的。 Linux OOM 杀手配置为在内存使用量超过 160MB 时终止进程。即使在高负载期间,我们的应用程序也永远不会超过 120MB,并且与其他一些活动的本机进程一起,我们保持在 OOM 限制内。
然而,事实证明 Java Runtime.getRuntime().exec() 方法是从 Java 执行外部进程的规范方法,它有一个 particularly unfortunate implementation on Linux 导致生成的子进程(暂时)需要相同的数量由于地址空间被复制,内存作为父进程。最终结果是,一旦我们执行 Runtime.getRuntime().exec(),我们的应用程序就会被 OOM 杀手杀死。
我们目前通过让一个单独的本机程序执行所有外部命令来解决此问题,并通过套接字与该程序进行通信。这不是最佳的。
在posting about this problem online 之后,我收到一些反馈,表明这不应该发生在“较新”版本的 Linux 上,因为它们使用写时复制实现了 posix fork() 方法,大概意味着它只会复制它需要的页面在需要时修改而不是立即修改整个地址空间。
我的问题是:
这是真的吗? 这是在内核、libc 实现中还是完全在其他地方? 从哪个版本的内核/libc/whatever 可以使用 fork() 的写时复制?【问题讨论】:
在 fork() 调用和后面的 exec() 之间需要的是 virtual,而不是 physical 内存。考虑到地址空间的大小与您的物理内存限制相比,我非常怀疑您的虚拟内存不足。 当然,我们并没有耗尽物理内存,但 Linux 的某些部分似乎认为我们已经耗尽了。 【参考方案1】:这几乎是 *nix(和 linux)自时间之初(或至少 mmus 之初)以来的工作方式。
要在 *nixes 上创建一个新进程,请调用 fork()。 fork() 使用其所有内存映射、文件描述符等创建调用进程的副本。内存映射是在写入时复制完成的,因此(在最佳情况下)实际上没有复制内存,只有映射。随后的 exec() 调用将当前内存映射替换为新可执行文件的映射。因此,fork()/exec() 是您创建新进程的方式,这也是 JVM 使用的方式。
需要注意的是,在繁忙的系统上存在大量进程,在子 exec() 导致大量内存被复制之前,父进程可能会继续运行一段时间,因为写入时复制。在 VM 中,内存可以移动很多,以促进垃圾收集器产生更多的复制。
“解决方法”是做你已经做过的事情,创建一个外部轻量级进程来处理生成新进程 - 或者使用比 fork/exec 更轻量级的方法来生成进程(哪个 linux 没有 -并且无论如何都需要改变jvm本身)。 Posix 指定了 posix_spawn() 函数,理论上可以在不复制调用进程的内存映射的情况下实现——但在 linux 上则不然。
【讨论】:
我会调用这个轻量级进程 ssh 并使用jcraft.com/jsch 连接到 localhost。【参考方案2】:好吧,我个人怀疑这是真的,因为 Linux 的 fork() 是通过写时复制完成的,因为上帝知道什么时候(至少,2.2.x 内核有它,它在 199x 的某个地方)。
由于 OOM 杀手被认为是一种相当粗糙的工具,已知会失火(fe,它没有必要杀死实际分配大部分内存的进程)并且应该仅作为最后的报告使用,它是我不清楚你为什么将它配置为在 160M 上触发。
如果你想对内存分配施加限制,那么 ulimit 是你的朋友,而不是 OOM。
我的建议是不理会 OOM(或完全禁用它),配置 ulimits,然后忘记这个问题。
【讨论】:
感谢您提供的信息,遗憾的是,我们无法控制该平台:其硬件、操作系统和配置大多是一成不变的(可以这么说,我们“只是”在其上部署)。跨度> 【参考方案3】:是的,即使是新版本的 Linux(我们使用的是 64 位 Red Hat 5.2)也是如此。大约 18 个月以来,我一直遇到子进程运行缓慢的问题,直到我阅读了您的问题并进行了测试以验证它之前,我才发现问题所在。
我们有一个 16 核的 32 GB 机器,如果我们使用 -Xms4g 和 -Xmx8g 等设置运行 JVM,并使用具有 16 个线程的 Runtime.exec() 运行子进程,我们将无法更快地运行我们的进程每秒大约 20 个进程调用。
在 Linux 中使用简单的“date”命令尝试大约 10,000 次。如果您添加分析代码来观察正在发生的事情,它会很快开始,但随着时间的推移会变慢。
阅读您的问题后,我决定尝试将内存设置降低到 -Xms128m 和 -Xmx128m。现在我们的进程以每秒大约 80 个进程调用的速度运行。我只更改了 JVM 内存设置。
它似乎并没有以我曾经耗尽内存的方式吸收内存,即使我尝试使用 32 个线程也是如此。只是必须以某种方式分配额外的内存,这会导致大量的启动(甚至可能是关闭)成本。
无论如何,似乎应该有一个设置来禁用这种行为 Linux 甚至在 JVM 中。
【讨论】:
【参考方案4】:1:是的。 2:这分为两个步骤:任何像fork()这样的系统调用都被glibc包装到内核中。系统调用的内核部分在 kernel/fork.c 3:我不知道。但我敢打赌你的内核有它。
当 32 位机器上的低内存受到威胁时,OOM 杀手就会发挥作用。我从来没有遇到过这个问题,但是有一些方法可以阻止 OOM。这个问题可能是一些 OOM 配置问题。
由于您使用的是 Java 应用程序,因此您应该考虑迁移到 64 位 Linux。那绝对应该解决它。只要安装了相关库,大多数 32 位应用程序都可以在 64 位内核上运行而不会出现问题。
您也可以尝试 32 位 fedora 的 PAE 内核。
【讨论】:
为什么迁移到 64 位 JVM 可以解决这个问题? @AlastairMcCormack 32 位 Linux 系统对用户空间和内核使用 3GB/1GB 拆分。 1GB 的一部分保留给需要通过系统调用访问内核空间的用户空间进程。这只有 128M。我相信 128M 内核和 3GB 用户空间的内存压力都会触发 OOM。虽然 3GB 空间中可能还有其他进程,但 JVM 更有可能成为最大的进程,因此成为 OOM 目标。此外,像 OPs JVM 这样的分叉过程具有更高的 OOM 分数,因为每个孩子都会增加父母的分数。 64 位没有内存分割或任何内核/用户分割。以上是关于从哪个 Linux 内核/libc 版本开始,Java Runtime.exec() 就内存而言是安全的?的主要内容,如果未能解决你的问题,请参考以下文章