如何重现Java OutOfMemoryError - GC超时限制

17

我的方法是创建数十万个本地集合,并用随机字符串填充它们,就像这样:

我的做法是创建10万个本地集合,然后用随机字符串填充它们,就像这样:

    SecureRandom random = new SecureRandom();
    for(int i = 0 ; i < 100000 ; i++){
        HashMap<String, String> map = new HashMap<String, String>();
        for(int j = 0 ; j < 30 ; j++){
            map.put(new BigInteger(130, random).toString(32), new BigInteger(130, random).toString(32));
        }
    }

我已经提供了-XX:+UseGCOverheadLimit JVM参数,但无法获得错误。是否有任何简单可靠的方法/技巧来获取此错误?


将地图保存在另一个在for循环之外声明的集合中,并在第一个for循环之前添加while(true) - Luiggi Mendoza
1
好问题!重现“堆OOM”足够容易,但是重现超限制的开销要困难得多...我想知道这是否可能!(故意这样做) - fge
将外部for循环改为while(true),这样就可以重现错误。 - VKPRO
@StephenC - 是的,完全正确。 - VKPRO
3个回答

13
由于您尚未接受任何答案,我将假设它们都对您无效。这里有一个有效的解决方法。但首先,请回顾一下触发此错误的条件

如果垃圾收集所花费的时间太长,即垃圾收集所占用的总时间超过98%且恢复的堆内存不到2%,则并行收集器会抛出OutOfMemoryError

因此,您必须几乎使用完整个堆,保持其分配状态,然后分配大量垃圾。向 Map 中添加大量内容无法实现此目的。

public static void main(String[] argv)
throws Exception
{
    List<Object> fixedData = consumeAvailableMemory();
    while (true)
    {
        Object data = new byte[64 * 1024 - 1];
    }
}


private static List<Object> consumeAvailableMemory()
throws Exception
{
    LinkedList<Object> holder = new LinkedList<Object>();
    while (true)
    {
        try
        {
            holder.add(new byte[128 * 1024]);
        }
        catch (OutOfMemoryError ex)
        {
            holder.removeLast();
            return holder;
        }
    }
}
consumeAvailableMemory() 方法会使用相对较小的内存块填充堆。"相对较小" 很重要,因为JVM会直接将 "大" 对象(根据我的经验,512k字节)放入旧生代,使年轻代保持空闲。
当我使用了大部分堆后,我只需分配和丢弃。在这个阶段中,较小的块大小很重要:我知道我至少有足够的内存进行一次分配,但可能不超过两次。这将保持GC处于活动状态。
运行此操作会在不到一秒钟内产生所需的错误。
> java -Xms1024m -Xmx1024m GCOverheadTrigger
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at GCOverheadTrigger.main(GCOverheadTrigger.java:12)

为了完整起见,这是我正在使用的JVM:

> java -version
java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)

现在我有一个问题要问你:为什么世界上你会想要这样做呢?


非常好的一种方式,可以永久性地使用 几乎 所有内存来进行复制。 - Andrzej Doyle
回答你的问题:我想测试一下JVM是否仍然可以在某种程度上从这种状态中恢复,也许需要一些监督员或其他服务的帮助。说实话,我也不理解这个异常的意义,因为JVM在这个异常之后很快就变得无响应了。我更倾向于期望JVM终止(采用快速失败的方法)。 - mkorszun
1
@mkorszun - 这就是异常的真正意义:关闭JVM而不是继续尝试释放空间。像大多数OOM错误一样,它表明您没有分配足够的堆来运行程序(有时“足够”是无限的,但更常见的是程序有一个实际的上限)。我在一个真实的程序中看到这个错误的唯一一次是,我们正在维护一组大型对象,这些对象将被传入的消息更新。这些消息的生命周期很短,因此会被不断收集。当我们增加堆时,错误消失了。 - kdgregory

2
这是:

HashMap<String, String> map = new HashMap<String, String>();

如果变量在循环内部被声明,且在循环迭代过程中没有对外部(长期)引用创建的映射表,则该变量的作用域限定在循环内部。因此,在每次循环迭代结束时,每个映射表都将有资格进行垃圾回收。

您需要在循环外部创建一个对象集合,并使用循环来填充该集合。


尽管这肯定会触发“堆OOM”,而不是超额限制,对吧? - fge

0

我认为这应该可以解决问题……只要你运行足够长的时间:

HashMap<Long, String> map = new HashMap<Long, String>();
for (long i = 0; true; i++) {
    for (int j = 0; j < 100; j++) {
        String s = "" + j;
        map.put(i, s);
    }
}

我正在做的是慢慢增加非垃圾的数量,同时创建大量垃圾。如果一直运行直到非垃圾几乎填满堆,垃圾回收器将达到一个时间百分比超过阈值的点。

它仍然会抛出java.lang.OutOfMemoryError:Java堆空间。 - mkorszun
增加内部循环的计数器。 - Stephen C
1
如果你想缩短运行时间,我猜你总是可以使用“-Xmx8M” ;) - fge
如果你想要“折磨”一下JVM,那么你应该尽可能地延长这种痛苦的时间。;-) - Stephen C
2
无法通过此代码获得20%以上的gc活动。我认为,与幸存者相比,它产生了太多的垃圾,这对于现代gc算法实际上是有益的。此外,这里的旧一代变化太少了。我认为,折磨JVM的好方法是像这样的方法。一旦达到一定的堆占用率,它可能会被某些删除所增强,然后,如果已配置此类限制,则应可靠地产生“超过限制”。 - Holger

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