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:+PrintGCDetailsGC 정보를 인쇄하여 추가 하십시오. 보시다시피 실제로 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을 받았습니까?
Vladislav Kysliy

1
내 시스템에서 이것을 재현 할 수 없습니다. 디버그 모드에서 GC가 작동하고 있음을 알 수 있습니다. 맵이 실제로 지워지고 있는지 디버그 모드로 확인할 수 있습니까?
magicmn

jre 1.8.0_212-b10 -Xmx200m 내가 첨부 한 gc 로그에서 더 자세한 내용을 볼 수 있습니다. thx
Dominic Peng

답변:


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)

훨씬 낫다.


당신의 대답을위한 Thx, 당신의 결론은 맞습니다. 페이로드를 1024 * 10000에서 1024 * 1000으로 줄이려고 시도하는 동안; 코드가 제대로 작동 할 수 있습니다. 그러나 나는 아직도 당신의 설명을 이해하지 못합니다. 의미로, WeakHashMap에서 공간을 해제해야하는 경우 gc를 적어도 두 번 수행해야합니다. 프리스트 시간은 맵에서 키를 수집하여 참조 큐에 추가하는 것입니다. 두 번째는 가치를 수집하는 것입니까? 그러나 제공 한 frist 로그에서 실제로 JVM은 이미 전체 gc를 두 번 사용했습니다.
Dominic Peng

"지도 값은 도달 할 수 있고 특정 작업이 호출 될 때지도 자체에 의해 지워집니다."라고 말합니다. 그들은 어디에서 접근 할 수 있습니까?
Andronicus

1
귀하의 경우 두 개의 GC 실행만으로는 충분하지 않습니다. 먼저 하나의 GC 실행이 필요합니다. 맞습니다. 그러나 다음 단계에서는지도 자체와의 상호 작용이 필요합니다. 찾아야 할 것은 java.util.WeakHashMap.expungeStaleEntries참조 대기열을 읽고 맵에서 항목을 제거하여 값에 도달 할 수없고 수집 대상이되는 방법입니다. 그 후에 만 ​​GC의 두 번째 패스는 약간의 메모리를 확보합니다. expungeStaleEntriesget / put / size와 같은 여러 경우 또는 일반적으로 맵으로 수행하는 거의 모든 경우에서 호출됩니다. 그게 캐치입니다.
촉수

1
@Andronicus는 WeakHashMap에서 가장 혼란스러운 부분입니다. 여러 번 다루었습니다. stackoverflow.com/questions/5511279/…
촉수

2
@Andronicus 이 답변 , 특히 후반에도 도움이 될 것입니다. 또한 이 Q & A
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상황이 약간 다를 수 있습니다. 이러한 큰 개체가 할당되면 ( 일반적으로 MB가 1 / 2MB 이상 )이를이라고합니다 humongous allocation. 이 경우 동시 GC가 트리거됩니다. 해당주기의 일부로 : 젊은 컬렉션이 트리거되고 Cleanup phase이벤트를에 게시하는 것을 처리 하는 항목이 시작되어 항목 ReferenceQueueWeakHashMap지워집니다.

따라서이 코드의 경우 :

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과 달리.

전화가 실제로 도움이되는 이유 와 함께 무엇 DisableExplicitGCExplicitGCInvokesConcurrent의미 하는지 읽고 System.gc이해할 수 System.gc있습니다.


1
Java 8은 기본적으로 G1GC를 사용하지 않습니다. 또한 OP의 GC 로그는 이전 세대에 병렬 GC를 사용하고 있음을 분명히 보여줍니다. 이러한 비 동시 컬렉터의 경우이 답변
Holger

@Holger 나는 오늘 아침 에이 답변을 실제로 검토하고 있다는 것을 깨닫기 위해 단지이 답변을 검토하고 있었고 ParalleGC, 나는 틀린 것을 증명 한 것에 대해 편집하고 죄송합니다 (감사합니다).
유진

1
"엄청난 할당"은 여전히 ​​올바른 힌트입니다. 비 동시 컬렉터를 사용하면 이전 세대가 가득 찼을 때 첫 번째 GC가 실행되므로 충분한 공간을 확보하지 못하면 치명적일 수 있습니다. 반대로 배열 크기를 줄이면 이전 세대의 메모리가 남아있을 때 어린 GC가 트리거되므로 수집기가 개체를 승격시키고 계속할 수 있습니다. 반면에 동시 콜렉터의 경우 힙이 소진되기 전에 gc를 트리거하는 것이 일반적이므로 새 JVM에서 실패하는 -XX:+UseG1GC것처럼 Java 8에서 작동하도록하십시오 -XX:+UseParallelOldGC.
Holger
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.