Java Runtime.getRuntime().exec()的替代方法

36

我有一组在Tomcat下运行的Web应用程序。 Tomcat已使用-Xmx参数配置了多达2 GB的内存。

许多Web应用程序需要执行一个任务,最终会使用以下代码:

Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(command);
process.waitFor();
...
我们遇到的问题与在Linux(Redhat 4.4和Centos 5.4)上创建“子进程”的方式有关。
我理解的是,为了创建该子进程,初始时需要在物理(非交换)系统内存池中保留与Tomcat使用量相等的一定数量的空闲内存。 当我们没有足够的空闲物理内存时,就会出现以下情况:
    java.io.IOException: error=12, Cannot allocate memory
     at java.lang.UNIXProcess.<init>(UNIXProcess.java:148)
     at java.lang.ProcessImpl.start(ProcessImpl.java:65)
     at java.lang.ProcessBuilder.start(ProcessBuilder.java:452)
     ... 28 more

我的问题是:

1)是否有可能去除需要与父进程相等的内存量在物理内存中空闲的要求?我正在寻找一个允许我指定子进程获得多少内存或允许Linux上的Java访问交换内存的答案。

2)如果没有解决方案#1,Runtime.getRuntime().exec()有哪些替代方法?我只能想到两个,都不是很理想。JNI(非常不理想)或在Java中重新编写我们调用的程序并使其成为自己的进程,Web应用程序以某种方式与其通信。必定还有其他替代方法。

3)这个问题是否有我没有看到的另一面可以潜在地修复它?降低Tomcat使用的内存量不是一个选项。增加服务器的内存始终是一种选择,但似乎更像是一个临时措施。

服务器正在运行Java 6。

编辑:我应该说明我不是在寻找特定于Tomcat的解决方案。我们在Web服务器上运行的任何Java应用程序都可能遇到此问题(有多个)。我仅仅是以Tomcat作为一个例子,因为它可能会分配最多的内存,而且它是我们第一次看到错误的地方。这是一个可复现的错误。

编辑:最终,我们通过在Java中重新编写系统调用的操作来解决了这个问题。我觉得我们很幸运,能够在不进行额外系统调用的情况下做到这一点。并不是所有进程都能够做到这一点,因此我仍然希望看到这个问题的实际解决方案。


1
相关链接:https://dev59.com/6HVC5IYBdhLWcg3wsTbv - BalusC
我并没有在那篇文章中看到任何解答我的问题的内容。其中一个提到的方法是“减少父进程使用的内存量”(不适用于我们),可以通过ulimit或java opts来实现。另一个方法与Luke上面的答案相同,即创建一个使用较少内存的单独进程。虽然这远非理想,但至少是可行的。 - twilbrand
你使用的是哪个JVM(Sun,OpenJDK ...?)这没帮助吗? https://dev59.com/GXNA5IYBdhLWcg3wBo9m - leonbloy
1
这是在提到的Linux操作系统上的Sun JVM。在那篇文章中,原帖作者说他用'echo 0 > /proc/sys/vm/overcommit_memory'修复了它,并请求有人解释为什么。但没有人回答。在我获得更多信息之前,我对使用这种方法持谨慎态度。谷歌搜索也没有帮助我。这个命令可能可以回答我的问题1或3,但我正在寻找有人解释这是做什么以及使用它的优缺点。 - twilbrand
那么,你最终做了什么?我认为这是一个很好的问题。请提供你的解决方案(尝试?)以造福我们其他人。 - pavanlimo
我相信这篇文章(虽然有点旧)讨论了这个问题。http://developers.sun.com/solaris/articles/subprocess/subprocess.html - Usman Saleem
6个回答

7

我在这篇文章中找到了一个解决方法,基本上的想法是在应用程序启动早期创建一个进程,并通过输入流与其通信,然后该子进程为您执行命令。

//you would probably want to make this a singleton
public class ProcessHelper
{
    private OutputStreamWriter output;
    public ProcessHelper()
    {
        Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec("java ProcessHelper");
        output = new OutputStreamWriter(process.getOutputStream());
    }
    public void exec(String command)
    {
        output.write(command, 0, command.length());
    }
}

那么你需要制作一个辅助的Java程序。
public class ProcessHelper
{
    public static void main(String[] args)
    {
         BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
         String command;
         while((command = in.readLine()) != null)
         {
             Runtime runtime = Runtime.getRuntime();
             Process process = runtime.exec(command);
         }
    }
}

我们所做的实质上是为您的应用程序创建了一个小型的“exec”服务器。如果您在应用程序早期初始化 ProcessHelper 类,它将成功创建此进程,然后您只需将命令传输到它,因为第二个进程要小得多,所以它应该始终成功。
您还可以使您的协议更加深入,例如返回退出代码、通知错误等。

这对于一个Web应用程序可能不起作用。Web应用程序在服务器运行时随时部署 - 占用任何数量的内存。 - Arne Burmeister
@Arne Burmeister - 如果您在共享的TomCat实例上运行此程序,则在运行时可能会为该进程分配任意大量的内存。但是,如果您在非共享的TomCat服务器上运行(这是唯一的应用程序),并且可以连接到应用程序启动以执行此操作,则应该可以正常工作。 - luke
正如我最近所述,我不是在寻找一个特定于Tomcat的答案。我们有多个Web应用程序以及多个其他Java进程,它们都可能遇到同样的问题。 - twilbrand

6

尝试使用ProcessBuilder。文档说这是现在启动子进程的“首选”方式。您还应考虑使用环境映射(文档在链接中)来指定新进程的内存允许量。我怀疑(但不确定)需要这么多内存的原因是它继承了tomcat进程的设置。使用环境映射可以允许您覆盖该行为。但是,请注意,启动进程非常依赖于操作系统,所以结果可能会有所不同。


我曾经研究过ProcessBuilder,特别是它修改环境的能力。实际上,您也可以使用Runtime来完成这个操作。然而,我找不到任何关于应该更改哪个环境变量以保持初始内存低的确切信息。我打印了一些进程的当前环境,但没有发现任何修改内存的参数。您能指出我如何使用环境变量控制内存分配吗?特别是在Linux中? - twilbrand
3
据我所知,ProcessBuilder 在 Linux 上也面临着相同的内存限制问题。 - kongo09
2
需要这么多内存的原因是,标准的“fork”算法用于启动新进程,会导致执行进程完全被临时复制。据说这是启动子进程的唯一跨平台方式。就我所知,ProcessBuilder只提供方便 - 它实际上并没有解决这个问题。 - robert
这到底有什么帮助呢?ProcessBuilder能让你控制最终启动新进程时使用的系统调用吗?这似乎不太可能,因此这并不能解决OP的问题。 - talonx

3

我并没有在那篇文章中看到任何回答我的问题的内容。其中一个操作是“减少父进程使用的内存量”(对我们来说不是一个选项),无论是使用ulimit还是java opts。另一个操作与卢克上面的答案相同,即创建一个使用较少内存的单独进程。这远非理想,但至少是可行的。 - twilbrand

1
另一种选择是运行一个单独的执行进程,它会监视文件或其他类型的“队列”。将所需的命令附加到该文件中,执行服务器会注意到它并运行命令,以某种方式将结果写入您可以获取的另一个位置。

关键在于执行消费者进程的占用空间很小,因此可以轻松进行分叉。 - Muhd
使用文件或其他公开可见的入口似乎可能会引入安全问题,不是吗?这会让攻击者以您的用户身份(在本例中为tomcat用户)执行任意代码。带有某种身份验证协议的套接字可能更好。实际上,该身份验证协议可能就是SSH... - Jolta

1

我目前发现的最佳解决方案是使用公钥进行安全壳。我使用 http://www.jcraft.com/jsch/ 创建了一个连接到 localhost,然后执行了如下命令。可能会有一些额外的开销,但对于我的情况而言,它运作良好。


1

不幸的是,这是专业版的一部分,如果这是您唯一需要的东西,则价格昂贵。您知道有任何免费替代品吗? - kongo09
尝试这个免费的 Tanuki Wrapper 替代品:http://sourceforge.net/projects/yajsw/forums/forum/810311/topic/4423982 - kongo09

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