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

2022-09-01 23:42:26

在工作中,我们的目标平台之一是运行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()进行写入复制?

答案 1

这几乎是*nix(和linux)自时间黎明以来的工作方式(或mmus的黎明)。

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

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

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


答案 2

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

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

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

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


推荐