如何检测 java 程序中的内存压力?

How to detect memory-pressure in a java program?

我有一个用 java 编写的批处理过程,它分析极长的标记序列(可能有数十亿甚至数万亿!)并观察二元语法模式(也称为词对)。

在此代码中,使用来自 Apache commons 的 ImmutablePair class 将二元语法表示为字符串对。我不会事先知道令牌的基数。它们可能非常重复,或者每个标记可能是完全独特的。

我可以放入内存的数据越多,分析就会越好!

但我绝对不能一次处理整个作业。因此,我需要将尽可能多的数据加载到缓冲区中,执行部分分析,将我的部分结果刷新到文件(或 API 或其他文件),然后清除缓存并重新开始。

我优化内存使用的一种方法是使用 Guava interners 对我的 String 实例进行去重。

现在,我的代码基本上是这样的:

int BUFFER_SIZE = 100_000_000;

Map<Pair<String, String>, LongAdder> bigramCounts = new HashMap<>(BUFFER_SIZE);

Interner<String> interner =  Interners.newStrongInterner();

String prevToken = null;
Iterator<String> tokens = getTokensFromSomewhere();
while (tokens.hasNest()) {
  String token = interner.intern(tokens.next());
  if (prevToken != null) {
    Pair<String, String> bigram = new ImmutablePair(prevToken, token);
    LongAdder bigramCount = bigramCounts.computeIfAbsent(
        bigram,
        (c) -> new LongAdder()
    );
    bigramCount.increment();
    // If our buffer is full, we need to flush!
    boolean tooMuchMemoryPressure = bigramCounts.size() > BUFFER_SIZE;
    if (tooMuchMemoryPressure) {
      // Analyze the data, and write the partial results somewhere
      doSomeFancyAnalysis(bigramCounts);
      // Clear the buffer and start over
      bigramCounts.clear();
    }
  }
  prevToken = token;
}

此代码的问题在于,这是一种非常粗略的确定是否存在 tooMuchMemoryPressure.

的方法

我想 运行 在许多不同类型的硬件上执行此作业,具有不同的内存量。无论实例如何,我都希望此代码自动调整以最大化内存消耗。

而不是使用像 BUFFER_SIZE 这样的硬编码常量(通过实验、启发式、猜测得出),我实际上只想询问 JVM 内存是否快满了。但这是一个非常复杂的问题,考虑到 mark/sweep 算法的复杂性,以及所有不同世代的收集器。

假设此批处理作业可能 运行 在具有不同可用内存量的各种不同机器上完成这样的事情,什么是好的通用方法?我不需要非常精确...我只是在寻找一个粗略的信号,以根据实际堆的状态知道我需要尽快刷新缓冲区。

初步了解进程堆 space 正在发生的事情的最简单方法是 Runtime.freeMemory() 以及 .maxMemory.totalMemory。然而,第一个不考虑垃圾,所以 under-estimation 充其量只是在 GC 开始之前可能完全误导。

假设对于您的应用程序“内存压力”基本上意味着“(很快)不够”,有趣的值是 GC 后立即释放内存。

这可以通过使用 GarbageCollectorMXBean 它提供 GcInfo GC 后的内存使用情况。

这个 bean 可以在 GC 之后被观察到,因为它是一个 NotificationEmitter,尽管这在 Javadoc 中没有被公布。 a longer example 之后的一些最小代码是

  void registerCallback() {
    List<GarbageCollectorMXBean> gcbeans =
      java.lang.management.ManagementFactory.getGarbageCollectorMXBeans();
    for (GarbageCollectorMXBean gcbean : gcbeans) {
      System.out.println(gcbean.getName());
      NotificationEmitter emitter = (NotificationEmitter) gcbean;
      emitter.addNotificationListener(this::handle, null, null);
    }
  }

  private void handle(Notification notification, Object handback) {
    if (!notification.getType()
      .equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
      return;
    }
    GarbageCollectionNotificationInfo info = GarbageCollectionNotificationInfo
      .from((CompositeData) notification.getUserData());
    GcInfo gcInfo = info.getGcInfo();
    gcInfo.getMemoryUsageAfterGc().forEach((name, memUsage) -> {
      System.err.println(name+ "->" + memUsage);
    });
  }

将有多个 memUsage 条目,这也会因 GC 的不同而有所不同。但是根据提供的值,usedcommittedmax 我们可以得出可用内存的上限,这应该再次给出 OP 要求的“粗略信号”。

doSomeFancyAnalysis 当然也需要它的新鲜内存份额,因此粗略估计每个二元语法需要分析多少,这可能是需要注意的限制。