尽管使用了 WeakHashMap,但 OutOfMemoryException

Dom*_*eng 10 java garbage-collection weak-references out-of-memory java-8

如果不调用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

如下,添加-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
Run Code Online (Sandbox Code Playgroud)

ten*_*cle 8

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
Run Code Online (Sandbox Code Playgroud)

直到最后一次尝试将值放入地图时才会触发 GC。

WeakHashMap 无法清除陈旧条目,直到映射键出现在引用队列中。并且映射键不会出现在引用队列中,直到它们被垃圾收集。新地图值的内存分配在地图有机会清除自己之前触发。当内存分配失败并触发 GC 时,会收集映射键。但为时已晚——没有足够的内存被释放来分配新的地图值。如果您减少有效负载,您可能最终会有足够的内存来分配新的映射值,并且陈旧的条目将被删除。

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

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)
Run Code Online (Sandbox Code Playgroud)

好多了。

  • @Andronicus [这个答案](/sf/answers/3549101041/),尤其是后半部分,可能也会有帮助。还有[此问答](/sf/ask/3847140251/)… (2认同)

Eug*_*ene 5

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

如果我将您的程序稍微更改为(在jdk-8with下运行-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
    }
}
Run Code Online (Sandbox Code Playgroud)

它会工作得很好。这是为什么?因为它为您的程序提供了足够的喘息空间,以便在WeakHashMap清除其条目之前进行新的分配。另一个答案已经解释了这是如何发生的。

现在,在G1GC,事情会有点不同。当分配如此大的对象时(通常超过 1/2 MB ),这将被称为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
    }
}
Run Code Online (Sandbox Code Playgroud)

我用 jdk-13 运行(G1GC默认值在哪里)

java -Xmx20m "-Xlog:gc*=debug" gc.WeakHashMapTest
Run Code Online (Sandbox Code Playgroud)

以下是部分日志:

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

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

 [2.082s][info ][gc,start] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)
Run Code Online (Sandbox Code Playgroud)

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


您现在可以看到,jdk-13当分配真正的大对象时,不会等待垃圾在旧区域堆积,而是触发并发GC 循环,从而节省了一天;与 jdk-8 不同。

您可能想阅读什么DisableExplicitGC和/或ExplicitGCInvokesConcurrent意思,再加上System.gc并理解为什么呼叫System.gc实际上在这里有帮助。