【问题标题】:How is it that when I call a method 10000 times it throws out of memory error?当我调用一个方法 10000 次时,它是如何抛出内存不足错误的?
【发布时间】:2020-02-07 23:09:17
【问题描述】:

情况

我的任务是在现场编码面试中实施一个字符串字谜问题。问题给出了两个字符串,为方法boolean isAnagram(String str1, String str2)编写逻辑。

解决方案

我提出了以下解决方案(mergeSort 是我自己的实现,containsChar 使用的是二进制搜索,这也是我自己的实现)

public static boolean isAnagram(String value, String valueToCompare) {
    String temp = valueToCompare.replaceAll("'", "").replaceAll(" ", "").toLowerCase();
    String t = value.replaceAll("'", "").replaceAll(" ", "").toLowerCase();
    if (t.length() == temp.length()) {
        char[] c = t.toCharArray();
        char[] orderedChars = MergeSort.mergeSort(temp.toCharArray());
        for (int i = 0; i < orderedChars.length ; i++) {
                if (!containsChar(orderedChars, c[i], 0, orderedChars.length - 1))
                    return false;
        }
        return true;
    }
    return false;
}

解决方案的效率是多余的,我更关心后台发生的事情。

问题

我提出解决方案后,面试官问我, 假设我有一台内存非常低的计算机,我想运行它 算法 10.000 次,随机字符串大小在 1000 到 10000 之间,你的代码会发生什么?我不知道该回答什么,所以他告诉我我会得到一个 OutOfMemoryError 异常。我知道(或者我至少认为)由于算法的效率,我会得到这样的例外。 所以我的问题是:

  1. 为什么会抛出 OutOfMemoryError 异常?
  2. 如果我调用该方法 1000 次,是否因为完成一次调用需要很长时间才能引发此类异常?
  3. 当我调用该方法 x 次时,后台发生了什么?

【问题讨论】:

标签: java algorithm memory-management out-of-memory java-memory-model


【解决方案1】:

让我们清楚这一点。

  • 面试官问了你一个假设的问题
  • 面试官没有正确指定条件(稍后会详细说明)
  • 采访者断言某事发生......没有证据,也没有办法验证该断言。

假设我有一台内存明显不足的计算机......所以他告诉我我会得到一个OutOfMemoryError 异常。

我认为面试官可能错了。

首先,您的代码没有明显的内存泄漏。我看不到,其他评论者也看不到。

您的解决方案代码确实会在每次调用时生成一些临时对象。我最多可以计算 6 个临时字符串和 1 或 2 个临时数组,以及可能由某些库方法创建的其他临时对象。您可能可以减少它... 如果值得在优化上花费开发人员时间

但临时对象本身不应导致 OOME。现代 Oracle / OpenJDK 垃圾收集器非常擅长收集短期对象。

除了一些病理情况:

场景 #1。

假设您已经处于内存不足的边缘。例如,假设在您开始 1000 次方法调用之前,在运行完整的 GC 之后,您只有少量空闲(eden)空间。

为了完成您的任务,它将生成大约 1000 次 x 10 个对象 x 10,000 字节的临时空间。大约是 100MB。

  • 如果您有 10MB 的 Eden 空间可用,这意味着您需要在短时间内收集大约 10 次 Eden 空间。

  • 如果您有 1MB 的 Eden 空间可用,这意味着您需要在短时间内收集大约 100 次 Eden 空间。

10 个 Eden 空间集合背靠背可能足以导致 OOME“超出开销限制”。如果是 100,则可能性更大。

但底线是,如果您运行得足够接近一个完整的堆,任何 段分配对象的代码都可能成为最后一根稻草。真正的问题是你的堆对于任务来说太小了......或者其他东西正在创建/保留太多长期对象。

场景 #2。

假设您的应用程序具有严格的延迟要求。为了实现这一点,您将 JVM 配置为使用低暂停收集器,并为收集器设置一些非常激进的延迟目标。而且你的内存也不多。

现在,如果您的应用程序生成过多垃圾的速度过快,则低暂停收集器可能无法跟上。如果你把它推到极限之外,GC 将回退到做一个 stop-the-world 收集来尝试恢复。你可能得到一个 OOME ......尽管我对此表示怀疑。但您肯定无法实现延迟目标。

但最重要的是,如果您的应用程序具有这样的要求,那么您必须在具有足够资源的机器上运行它;即足够的备用内存,(并行)GC可以跟上的足够核心。您可能会将您的 isAnagram 方法设计为 (erm) 在创建临时对象的方式上更加小心 ...但您会事先知道您需要这样做。

回顾

回到你的面试官提出的问题(由你转达):

  • 面试官没有说有多少可用堆空间,所以我们不能说场景 #1 是否适用。但如果确实如此,真正的问题将是堆大小与问题之间的不匹配,或者是应用程序中其他地方的内存泄漏。

  • 面试官没有提到延迟限制。即使它们存在,第一步也是指定硬件并使用适当的(即实际的)JVM GC 设置。

  • 如果您确实遇到了问题(OOME、错过延迟目标),那么您开始寻找解决方案。使用内存分析来识别问题的性质(例如,它是由临时对象、长期对象、内存泄漏等引起的)并追踪有问题的对象的来源。

    不要仅仅假设某段代码导致 OOME ......就像面试官所做的那样。过早的优化是个坏主意。

【讨论】:

    【解决方案2】:

    让它工作。改正它。让它快点。

    现在考虑性能或内存使用情况还为时过早。您的方法返回误报,因为它只检查第一个单词中的每个字母是否包含在第二个单词中。

    通过此检查,'aaa''abc' 被认为是字谜,而不是 'abc''aaa'

    这是一个完整的类来测试你的代码:

    import java.util.Arrays;
    
    
    public class AnagramTest
    {
        public static void main(String[] args) {
            String[][] anagrams = {
                    { "abc", "cba" },
                    { "ABC", "CAB" },
                    { "Clint Eastwood", "Old West action" }
            };
    
            for (String[] words : anagrams) {
                if (isAnagram(words[0], words[1])) {
                    System.out.println(".");
                } else {
                    System.out.println(
                            "OH NO! '" + words[0] + "' and '" + words[1] + "' are anagrams but isAnagram returned false.");
                }
            }
    
            String[][] notAnagrams = {
                    { "hello", "world" },
                    { "aabb", "aab" },
                    { "abc", "aaa" },
                    { "aaa", "abc" },
                    { "aab", "bba" },
                    { "aab", "bba" },
            };
    
            for (String[] words : notAnagrams) {
                if (isAnagram(words[0], words[1])) {
                    System.out.println(
                            "OH NO! '" + words[0] + "' and '" + words[1] + "' are not anagrams but isAnagram returned true.");
                } else {
                    System.out.println(".");
                }
            }
        }
    
        public static boolean isAnagram(String value, String valueToCompare) {
            String temp = valueToCompare.replaceAll("'", "").replaceAll(" ", "").toLowerCase();
            String t = value.replaceAll("'", "").replaceAll(" ", "").toLowerCase();
            if (t.length() == temp.length()) {
                char[] c = t.toCharArray();
                char[] orderedChars = mergeSort(temp.toCharArray());
                for (int i = 0; i < orderedChars.length; i++) {
                    if (!containsChar(orderedChars, c[i], 0, orderedChars.length - 1))
                        return false;
                }
                return true;
            }
            return false;
        }
    
        // Dummy method. Warning: sorts chars in place.
        private static char[] mergeSort(char[] chars) {
            Arrays.sort(chars);
            return chars;
        }
    
        // replace with your binary search if you want.
        private static boolean containsChar(char[] orderedChars, char c, int m, int n) {
            for (int i = m; i <= n; i++) {
                if (orderedChars[i] == c) {
                    return true;
                }
            }
            return false;
        }
    }
    

    它输出:

    .
    .
    .
    .
    .
    .
    OH NO! 'aaa' and 'abc' are not anagrams but isAnagram returned true.
    OH NO! 'aab' and 'bba' are not anagrams but isAnagram returned true.
    OH NO! 'aab' and 'bba' are not anagrams but isAnagram returned true.
    

    这是一个应该通过所有测试的示例实现:

    public static boolean isAnagram(String word1, String word2) {
        word1 = word1.replaceAll("'", "").replaceAll(" ", "").toLowerCase();
        word2 = word2.replaceAll("'", "").replaceAll(" ", "").toLowerCase();
        return Arrays.equals(mergeSort(word1.toCharArray()), mergeSort(word2.toCharArray()));
    }
    

    【讨论】:

      【解决方案3】:

      我的最佳猜测:

      • 您的 MergeSort 有问题,您没有向我们展示;
      • 并非每次输入都会发生这种情况,因此面试官希望您使用随机输入运行 10000 次,以使其以高概率发生;
      • 此问题可能会导致您的合并排序过于深入地递归。可能是 O(N) 而不是 O(log N) 深度,或者可能是无限递归;和
      • 您的合并排序不必要地在每个递归调用中分配一个新的临时数组。由于它们太多,这会导致内存不足错误。

      【讨论】:

      • 是的,我认为他想推动我找到更好的解决方案,而无需排序。
      猜你喜欢
      • 2019-10-09
      • 2014-10-13
      • 2018-04-17
      • 2020-10-17
      • 1970-01-01
      • 1970-01-01
      • 2022-11-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多