尽管使用了WeakHashMap,但仍发生OutOfMemoryException


9

如果不调用System.gc(),系统将抛出OutOfMemoryException。我不知道为什么需要System.gc()显式呼叫;JVM应该gc()自称对吧?请指教。

以下是我的测试代码:

public static void main(String[] args) throws InterruptedException {
    WeakHashMap<String, int[]> hm = new WeakHashMap<>();
    int i  = 0;
    while(true) {
        Thread.sleep(1000);
        i++;
        String key = new String(new Integer(i).toString());
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[1024 * 10000]);
        key = null;
        //System.gc();
    }
}

如下所示,添加-XX:+PrintGCDetails以打印出GC信息;如您所见,实际上,JVM尝试执行完整的GC运行,但是失败;我仍然不知道原因。如果我取消注释该System.gc();行,结果是肯定的,这很奇怪:

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
[GC (Allocation Failure) --[PSYoungGen: 48344K->48344K(59904K)] 168344K->168352K(196608K), 0.0090913 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 48344K->41377K(59904K)] [ParOldGen: 120008K->120002K(136704K)] 168352K->161380K(196608K), [Metaspace: 5382K->5382K(1056768K)], 0.0380767 secs] [Times: user=0.09 sys=0.03, real=0.04 secs] 
[GC (Allocation Failure) --[PSYoungGen: 41377K->41377K(59904K)] 161380K->161380K(196608K), 0.0040596 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 41377K->41314K(59904K)] [ParOldGen: 120002K->120002K(136704K)] 161380K->161317K(196608K), [Metaspace: 5382K->5378K(1056768K)], 0.0118884 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at test.DeadLock.main(DeadLock.java:23)
Heap
 PSYoungGen      total 59904K, used 42866K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 51712K, 82% used [0x00000000fbd80000,0x00000000fe75c870,0x00000000ff000000)
  from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
  to   space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
 ParOldGen       total 136704K, used 120002K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
  object space 136704K, 87% used [0x00000000f3800000,0x00000000fad30b90,0x00000000fbd80000)
 Metaspace       used 5409K, capacity 5590K, committed 5760K, reserved 1056768K
  class space    used 576K, capacity 626K, committed 640K, reserved 1048576K

什么是jdk版本?您是否使用任何-Xms和-Xmx参数?您在哪一步获得了OOM?
弗拉迪斯拉夫·基斯利

1
我无法在系统上重现此内容。在调试模式下,我可以看到GC正在完成它的工作。您是否可以在调试模式下检查地图是否被清除?
magicmn

jre 1.8.0_212-b10 -Xmx200m您可以从我附加的gc日志中看到更多详细信息;thx
多米尼克·彭

Answers:


7

JVM会自行调用GC,但在这种情况下,它太少了,太迟了。在这种情况下,清除内存的不仅是GC。映射值很容易达到,并且在对映射值进行某些操作时会被映射本身清除。

如果您打开GC事件(XX:+ PrintGC),则输出如下:

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
add new element 6
add new element 7
[GC (Allocation Failure)  2407753K->2400920K(2801664K), 0.0123285 secs]
[GC (Allocation Failure)  2400920K->2400856K(2801664K), 0.0090720 secs]
[Full GC (Allocation Failure)  2400856K->2400805K(2590720K), 0.0302800 secs]
[GC (Allocation Failure)  2400805K->2400805K(2801664K), 0.0069942 secs]
[Full GC (Allocation Failure)  2400805K->2400753K(2620928K), 0.0146932 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

直到最后一次尝试将价值放入映射中时,才会触发GC。

直到映射键出现在参考队列上,WeakHashMap才能清除陈旧的条目。映射键只有在被垃圾回收后才会出现在参考队列上。在地图有机会自行清除之前,会触发为新地图值分配内存。当内存分配失败并触发GC时,将收集映射键。但这太少了,太迟了-没有足够的内存来分配新的映射值。如果减少有效负载,则可能会获得足够的内存来分配新的映射值,并且过时的条目将被删除。

另一种解决方案是将值本身包装到WeakReference中。这将允许GC清除资源,而无需等待地图自行完成。这是输出:

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
add new element 6
add new element 7
[GC (Allocation Failure)  2407753K->2400920K(2801664K), 0.0133492 secs]
[GC (Allocation Failure)  2400920K->2400888K(2801664K), 0.0090964 secs]
[Full GC (Allocation Failure)  2400888K->806K(190976K), 0.1053405 secs]
add new element 8
add new element 9
add new element 10
add new element 11
add new element 12
add new element 13
[GC (Allocation Failure)  2402096K->2400902K(2801664K), 0.0108237 secs]
[GC (Allocation Failure)  2400902K->2400838K(2865664K), 0.0058837 secs]
[Full GC (Allocation Failure)  2400838K->1024K(255488K), 0.0863236 secs]
add new element 14
add new element 15
...
(and counting)

好多了。


谢谢您的回答,看来您的结论是正确的;而我试图将有效载荷从1024 * 10000减少到1024 * 1000; 该代码可以正常工作;但是我还是不太了解你的解释;根据您的意思,如果需要从WeakHashMap中释放空间,则应至少执行两次gc;第一时间是从地图上收集密钥,并将其添加到参考队列中;第二次是收集价值吗?但是实际上,从您提供的第一份日志来看,JVM已经使用了完整的gc两次;
Dominic Peng

您的意思是,“映射值很容易到达,并且在对映射值进行某些操作时会被映射本身清除”。从哪里可以到达?
Andronicus

1
在您的情况下仅运行两个GC是不够的。首先,您需要运行一次GC,这是正确的。但是下一步将需要与地图本身进行一些交互。您应该寻找的是一种方法java.util.WeakHashMap.expungeStaleEntries,该方法读取参考队列并从映射中删除条目,从而使值不可访问并易于收集。只有这样做之后,第二遍GC才会释放一些内存。expungeStaleEntries在诸如get / put / size之类的许多情况下会被调用,或者几乎是您通常对地图所做的所有事情。这就是陷阱。
触手

1
@Andronicus,这是迄今为止WeakHashMap中最令人困惑的部分。它被覆盖了多次。stackoverflow.com/questions/5511279/...
触手

2
@Andronicus 这个答案,尤其是后半部分,也可能会有所帮助。也是这个问答环节
Holger

5

另一个答案确实是正确的,我编辑了我的。作为一个小的附录,G1GC不会表现出这种行为,不像ParallelGC; 这是的默认设置java-8

如果我将您的程序稍微更改为(在jdk-8上运行-Xmx20m),您认为会发生什么

public static void main(String[] args) throws InterruptedException {
    WeakHashMap<String, int[]> hm = new WeakHashMap<>();
    int i = 0;
    while (true) {
        Thread.sleep(200);
        i++;
        String key = "" + i;
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[512 * 1024 * 1]); // <--- allocate 1/2 MB
    }
}

它将正常工作。这是为什么?因为它为您的程序提供了足够的喘息空间,以便在WeakHashMap清除其条目之前进行新的分配。另一个答案已经说明了这种情况。

现在,在中G1GC,情况会有所不同。当分配了如此大的对象(通常大于1/2 MB )时,这将称为a humongous allocation。发生这种情况时,将触发并发 GC。作为该周期的一部分:将触发一个年轻集合,并Cleanup phase会启动一个,将把事件发布到ReferenceQueue,以便WeakHashMap清除其条目。

因此,对于此代码:

public static void main(String[] args) throws InterruptedException {
    Map<String, int[]> hm = new WeakHashMap<>();
    int i = 0;
    while (true) {
        Thread.sleep(1000);
        i++;
        String key = "" + i;
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[1024 * 1024 * 1]); // <--- 1 MB allocation
    }
}

我使用jdk-13(G1GC默认位置为)运行

java -Xmx20m "-Xlog:gc*=debug" gc.WeakHashMapTest

这是部分日志:

[2.082s][debug][gc,ergo] Request concurrent cycle initiation (requested by GC cause). GC cause: G1 Humongous Allocation

这已经做了一些不同的事情。它启动了concurrent cycle您的应用程序运行时完成),因为其中有一个G1 Humongous Allocation。作为此并发周期的一部分,它将执行一个年轻的GC周期(这会在运行时停止您的应用程序)

 [2.082s][info ][gc,start] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)

作为该年轻 GC的一部分,它还清除了巨大的区域这就是缺陷


现在您可以看到,jdk-13分配了真正的大对象时,您不必等待垃圾在旧区域堆积,而是触发了并发的 GC周期,从而节省了时间。不像jdk-8。

您可能想阅读什么DisableExplicitGC和/或什么ExplicitGCInvokesConcurrent意思,System.gc并理解为什么System.gc在这里打电话实际上会有所帮助。


1
Java 8默认不使用G1GC。OP的GC日志也清楚地表明,它正在将并行GC用于旧版本。对于这样的非并发收集器,就像这个答案中描述的那样简单
Holger

@Holger我今天早上在审查这个答案,才意识到它的确是ParalleGC我编辑过,对不起(并感谢您)证明我做错了。
尤金

1
“巨大的分配”仍然是正确的提示。对于非并发收集器,这意味着第一个GC将在旧一代已满时运行,因此无法回收足够的空间将使其致命。相反,当减小阵列大小时,如果旧一代中仍然有内存,则会触发一个年轻的GC,因此收集器可以提升对象并继续。另一方面,对于并发收集器,在堆耗尽之前触发gc是正常的,因此-XX:+UseG1GC使其在Java 8中工作,就像-XX:+UseParallelOldGC使其在新JVM中失败一样。
霍尔格
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.