如何估计 JVM 是否有足够的空闲内存用于特定的数据结构?

How to estimate if the JVM has enough free memory for a particular data structure?

我有以下情况:有几台机器组成一个集群。客户端可以加载数据集,我们需要 select 将加载数据集的节点,如果没有一台机器可以适合数据集,则拒绝加载/避免 OOM 错误。

我们目前所做的:我们现在将 entry count 放入数据集中,并将 memory to be used 估计为 entry count * empirical factor(手动确定)。然后检查这是否低于可用内存(由 Runtime.freeMemory() 获得),如果是,则加载它(否则在其他节点上重做该过程/报告没有可用容量)。

这种方法的问题是:

这个问题有更好的解决方案吗?

empirical factor 可以计算为构建步骤并放在属性文件中。

虽然 freeMemory() 几乎总是小于 GC 后可用的数量,但您可以检查它是否可用并调用 System.gc() 如果 maxMemory()表示可能有很多。

注意:在生产中使用 System.gc() 只会造成 very rare situations,而且通常它经常被错误使用,导致性能下降并掩盖真正的问题。

我会避免触发 OOME 除非你是 运行 是一个 JVM,你可以根据需要重新启动。

我的解决方案:

  1. 如果除你的程序外没有其他进程运行,则将Xmx设置为物理机RAM的90%-95%。对于 32 GB RAM 机器,将 Xmx 设置为 27MB - 28MB.

  2. 使用一种好的 gc 算法 - CMSG1GC 并微调相关参数。 I prefer G1GC if you need more than 4 GB RAM for your application。选择G1GC的请参考这个问题:

    Agressive garbage collector strategy

    Reducing JVM pause time > 1 second using UseConcMarkSweepGC

  3. 自行计算内存使用上限,而不是检查可用内存。添加已用内存和要分配的内存。 Subtract it from your own cap like 90% of Xmx。如果您仍有可用内存,请授予内存分配请求。

正如您正确指出的那样,使用 freeMemory 不会告诉您 Java 垃圾收集可以释放的内存量。您可以 运行 加载测试并了解 JVM 堆使用模式和内存分配,使用 JConsole、VisualVM、jstat 和 JVM 的 printGCStats 选项等工具取消分配模式。这将提供有关更准确地计算 empirical factor 的想法,基本上了解您的 java 应用程序可以处理的负载模式。接下来是选择正确的 GC 并调整基本的 GC 设置以提高效率。这不是一个快速的解决方案,但从长远来看可能是一个更好的解决方案。

另一种方法是通过 -XX:OnOutOfMemoryError="kill -9 %p" JVM 设置杀死你的 JVM,一旦 OOM 发生,然后编写,resue 一个简单的进程监控脚本来在你的 JVM 关闭时启动它。

另一种方法是将每个数据加载隔离在其自己的 JVM 中。您只需预定义每个 JVM 的最大堆大小等,并设置每个主机的 JVM 数量,使每个 JVM 都能占用其全部最大堆大小。这将使用更多的资源——这意味着你不能通过塞入更多低内存数据负载来利用内存的最后一个字节——但它极大地简化了问题(并降低了出错的风险),它使告诉 when/whether 您需要添加新主机变得可行,最重要的是,它减少了任何一个客户端对所有其他客户端的影响。

使用这种方法,给定的 JVM 是 "busy" 或 "available"。

任何给定的数据加载完成后,相关的 JVM 可以声明自己可用于新的数据加载,也可以直接关闭。 (无论哪种方式,您都希望有一个单独的进程来监视 JVM 并确保正确的数字始终是 运行。)

Clients can load data-sets and we need to select the node on which the dataset will be loaded and refuse to load / avoid an OOM error if there is no one machine which could fit the dataset.

这是一个作业调度问题,即 我的资源有限,我们如何最好地利用它们。 我会遇到 OOM 问题快结束了。

我们有一个主要因素,即 RAM,但调度问题的解决方案取决于许多因素,即...

  1. 作业是小的还是大的,即在一个或两个或三个节点上有 hundreds/thousands 个 运行。想想 Linux 调度程序。

  2. 他们需要在特定时间范围内完成吗?实时调度程序。

鉴于我们在工作开始时所知道的一切,我们能否预测工作将在某个时间范围内何时结束?如果我们可以预测在节点 X 上我们每 15 - 20 秒释放 100MB,我们就有办法在该节点上安排 200Mb 的作业,即我有信心在 40 秒内完成 200Mb 的 space在该节点上,40 秒是提交作业的人或机器可接受的限制。

假设我们有一个函数如下。

predicted_time predict(long bytes[, factors]); 

factors 是我上面提到的我们需要考虑的其他事项,对于每个应用程序,您都可以添加一些内容以适合您的场景。

计算predicted_time时会赋予因素权重。

predicted_time 是这个节点认为从现在开始它可以服务这个任务的毫秒数(可以是任何时间单位),给你最小数字的节点是应该安排作业的节点.然后,您可以在我们有一个任务队列的地方按如下方式使用此函数,即在以下代码中 this.nodes[i] 代表一个 JVM 实例。

private void scheduleTask() {
  while(WorkEvent()) {
        while(!this.queue.isEmpty()) {
            Task t = this.queue.poll();
            for (int i = 0; i < this.maxNodes; i++) {
                long predicted_time = this.nodes[i].predict(t);
                if (predicted_time < 0) {
                    boolean b = this.queue.offer(t);
                    assert(b);
                    break;
                }
                if (predicted_time <= USER_EXPERIENCE_DELAY) {
                    this.nodes[i].addTask(t);
                    break;
                }
                alert_user(boolean b = this.queue.offer(t);
                assert(b);
            }
        }
    }
}

如果 predicted_time < 0 我们有错误,我们会重新安排工作,实际上我们想知道原因,但这并不难添加。如果 predicted_time <= USER_EXPERIENCE_DELAY 可以安排作业。

这如何避免OOM

我们可以从我们的调度程序中收集我们想要的任何统计数据,即正确调度了多少个大小为 X 的作业,目的是减少错误并使其随着时间的推移变得更加可靠,即减少我们告诉的次数客户无法为他们的工作提供服务。我们所做的是将问题减少到我们可以统计地改进以获得最佳解决方案的问题。

Clients can load data-sets and we need to select the node on which the dataset will be loaded and refuse to load / avoid an OOM error if there is no one machine which could fit the dataset.

这是一个作业调度问题,即 我的资源有限,我们如何最好地利用它们。 我会遇到 OOM 问题快结束了。

我们有一个主要因素,即 RAM,但调度问题的解决方案取决于许多因素,即...

  1. 作业是小的还是大的,即在一个或两个或三个节点上有 hundreds/thousands 个 运行。想想 Linux 调度程序。

  2. 他们需要在特定时间范围内完成吗?实时调度程序。

鉴于我们在工作开始时所知道的一切,我们能否预测工作将在某个时间范围内何时结束?如果我们可以预测在节点 X 上我们每 15 - 20 秒释放 100MB,我们就有办法在该节点上安排 200Mb 的作业,即我有信心在 40 秒内完成 200Mb 的 space在该节点上,40 秒是提交作业的人或机器可接受的限制。

假设我们有一个函数如下。

predicted_time predict(long bytes[, factors]); 

factors 是我们需要考虑的其他事项,我在上面提到过,对于每个应用程序,您都可以添加一些内容以适应您的场景,即有多少因素取决于您。

计算predicted_time时会赋予因素权重。

predicted_time 是这个节点认为从现在开始它可以服务这个任务的毫秒数(可以是任何时间单位),给你最小数字的节点是应该安排作业的节点.然后,您可以在我们有一个任务队列的地方按如下方式使用此函数,即在以下代码中 this.nodes[i] 代表一个 JVM 实例。

private void scheduleTask() {
  while(WorkEvent()) {
        while(!this.queue.isEmpty()) {
            Task t = this.queue.poll();
            for (int i = 0; i < this.maxNodes; i++) {
                long predicted_time = this.nodes[i].predict(t);
                if (predicted_time < 0) {
                    boolean b = this.queue.offer(t);
                    assert(b);
                    break;
                }
                if (predicted_time <= USER_EXPERIENCE_DELAY) {
                    this.nodes[i].addTask(t);
                    break;
                }
                alert_user(boolean b = this.queue.offer(t);
                assert(b);
            }
        }
    }
}

如果 predicted_time < 0 我们有错误,我们会重新安排工作,实际上我们想知道原因,但这并不难添加。如果 predicted_time <= USER_EXPERIENCE_DELAY 可以安排作业。

这如何避免OOM

我们可以从我们的调度程序中收集我们想要的任何统计数据,即正确调度了多少个大小为 X 的作业,目的是减少错误并使其随着时间的推移变得更加可靠,即减少我们告诉的次数客户说他们的工作无法得到服务或失败。我们所做的或至少正在尝试的是将问题减少到我们可以统计改进以获得最佳解决方案的问题。

an alternative would be to "just try to load the dataset" (and back out if an OOM is thrown) however once an OOM is thrown, you potentially corrupted other threads running in the same JVM and there is no graceful way of recovering from it.

在 JVM 中没有处理和恢复 OOME 的好方法,但是有办法在 OOM 发生之前做出反应。 Java 有 java.lang.ref.SoftReference 保证在虚拟机抛出 OutOfMemoryError 之前已被清除。这一事实可用于 OOM 的早期预测。例如,如果触发预测,可以中止数据加载。

    ReferenceQueue<Object> q = new ReferenceQueue<>();
    SoftReference<Object> reference = new SoftReference<>(new Object(), q);
    q.remove();
    // reference removed - stop data load immediately

可以使用 -XX:SoftRefLRUPolicyMSPerMB 标志调整灵敏度(对于 Oracle JVM)。 解决方案并不理想,它的有效性取决于多种因素 - 是否在代码中使用了其他软引用、如何调整 GC、JVM 版本、火星上的天气...但如果你幸运的话它会有所帮助。