OutOfMemoryException mặc dù sử dụng WeakHashMap


9

Nếu không gọi System.gc(), hệ thống sẽ đưa ra OutOfMemoryException. Tôi không biết tại sao tôi cần phải gọi System.gc()một cách rõ ràng; JVM nên gọi gc()chính nó, phải không? Xin tư vấn.

Sau đây là mã kiểm tra của tôi:

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();
    }
}

Như sau, thêm -XX:+PrintGCDetailsđể in ra thông tin GC; như bạn thấy, trên thực tế, JVM cố gắng thực hiện một hoạt động đầy đủ của GC, nhưng không thành công; Tôi vẫn không biết lý do. Điều rất lạ là nếu tôi bỏ ghi chú System.gc();thì kết quả rất khả quan:

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

phiên bản jdk gì? Bạn có sử dụng bất kỳ tham số -Xms và -Xmx nào không? bước nào bạn có OOM?
Vladislav Kysliy

1
Tôi không thể tái tạo điều này trên hệ thống của tôi. Trong chế độ gỡ lỗi, tôi có thể thấy rằng GC đang thực hiện công việc của mình. Bạn có thể kiểm tra trong chế độ gỡ lỗi nếu Bản đồ thực sự bị xóa hay không?
Magicmn

jre 1.8.0_212-b10 -Xmx200m Bạn có thể xem chi tiết hơn từ nhật ký gc mà tôi đã đính kèm; thx
Dominic Peng

Câu trả lời:


7

JVM sẽ tự gọi GC, nhưng trong trường hợp này sẽ quá muộn. Không chỉ có GC, người chịu trách nhiệm xóa bộ nhớ trong trường hợp này. Các giá trị bản đồ có thể truy cập mạnh và được xóa bằng chính bản đồ khi các hoạt động nhất định được gọi trên đó.

Đây là đầu ra nếu bạn bật các sự kiện 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 không được kích hoạt cho đến lần thử cuối cùng để đưa giá trị vào bản đồ.

WeakHashMap không thể xóa các mục cũ cho đến khi các khóa bản đồ xuất hiện trên hàng đợi tham chiếu. Và các khóa bản đồ không xảy ra trên hàng đợi tham chiếu cho đến khi chúng được thu gom rác. Cấp phát bộ nhớ cho giá trị bản đồ mới được kích hoạt trước khi bản đồ có bất kỳ cơ hội nào để tự xóa. Khi cấp phát bộ nhớ không thành công và kích hoạt GC, các khóa bản đồ sẽ được thu thập. Nhưng quá muộn - không đủ bộ nhớ để giải phóng giá trị bản đồ mới. Nếu bạn giảm tải, có thể bạn sẽ có đủ bộ nhớ để phân bổ giá trị bản đồ mới và các mục cũ sẽ bị xóa.

Một giải pháp khác có thể là tự gói các giá trị vào WeakReference. Điều này sẽ cho phép GC xóa tài nguyên mà không cần chờ bản đồ tự làm. Đây là đầu ra:

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)

Tốt hơn nhiều.


Thx cho câu trả lời của bạn, có vẻ như kết luận của bạn là chính xác; trong khi tôi cố gắng giảm tải từ 1024 * 10000 xuống 1024 * 1000; mã có thể hoạt động tốt; nhưng tôi VẪN không hiểu lắm về lời giải thích của bạn; như ý nghĩa của bạn, nếu cần giải phóng không gian từ WeakHashMap, nên thực hiện gc ít nhất hai lần; thời gian đầu tiên là thu thập các khóa từ bản đồ và thêm chúng vào hàng đợi tham chiếu; Lần thứ hai là thu thập giá trị? nhưng từ nhật ký đầu tiên mà bạn cung cấp, thực ra, JVM đã lấy gc đầy đủ hai lần;
Đaminh Bành

Bạn đang nói rằng "Các giá trị bản đồ có thể truy cập mạnh mẽ và bị xóa bởi chính bản đồ khi các hoạt động nhất định được gọi trên đó." Họ có thể đến từ đâu?
Andronicus

1
Sẽ không đủ nếu chỉ có hai lần chạy GC trong trường hợp của bạn. Trước tiên, bạn cần một lần chạy GC, điều đó đúng. Nhưng bước tiếp theo sẽ yêu cầu một số tương tác với chính bản đồ. Những gì bạn nên tìm là phương pháp java.util.WeakHashMap.expungeStaleEntriesđọc hàng đợi tham chiếu và xóa các mục khỏi bản đồ, do đó làm cho các giá trị không thể truy cập được và phải được thu thập. Chỉ sau khi hoàn thành, lần thứ hai của GC sẽ giải phóng bộ nhớ. expungeStaleEntriesđược gọi trong một số trường hợp như get / put / size hoặc khá nhiều thứ bạn thường làm với bản đồ. Đó là bắt.
xúc tu

1
@Andronicus, đây là phần khó hiểu nhất của WeakHashMap. Nó đã được bảo hiểm nhiều lần. stackoverflow.com/questions/5511279/ trộm
xúc tu

2
@Andronicus câu trả lời này , đặc biệt là nửa sau, cũng có thể hữu ích. Ngoài ra câu hỏi và trả lời này
Holger

5

Câu trả lời khác thực sự chính xác, tôi đã chỉnh sửa của tôi. Là một phụ lục nhỏ, G1GCsẽ không thể hiện hành vi này, không giống như ParallelGC; đó là mặc định dướijava-8 .

Bạn nghĩ gì sẽ xảy ra nếu tôi hơi thay đổi chương trình của bạn để (chạy dưới jdk-8với -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
    }
}

Nó sẽ chỉ làm việc tốt. Tại sao vậy? Bởi vì nó cung cấp cho chương trình của bạn vừa đủ phòng thở để phân bổ mới xảy ra, trước đóWeakHashMap xóa các mục của nó. Và câu trả lời khác đã giải thích làm thế nào điều đó xảy ra.

Bây giờ, trong G1GC, mọi thứ sẽ khác một chút. Khi một đối tượng lớn như vậy được phân bổ (hơn 1/2 một MB thường ), điều này sẽ được gọi là một humongous allocation. Khi điều đó xảy ra, một GC đồng thời sẽ được kích hoạt. Là một phần của chu trình đó: một bộ sưu tập trẻ sẽ được kích hoạt và một bộ sưu tập Cleanup phasesẽ được khởi xướng sẽ đảm nhiệm việc đăng sự kiện lên ReferenceQueue, do đóWeakHashMap xóa các mục của nó.

Vì vậy, đối với mã này:

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
    }
}

mà tôi chạy với jdk-13 (nơi G1GCmặc định)

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

Đây là một phần của nhật ký:

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

Điều này đã làm một cái gì đó khác nhau. Nó bắt đầu một concurrent cycle(được thực hiện trong khi ứng dụng của bạn đang chạy), bởi vì đã có một G1 Humongous Allocation. Là một phần của chu trình đồng thời này, nó thực hiện một chu trình GC trẻ ( dừng ứng dụng của bạn trong khi chạy)

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

Là một phần của GC trẻ đó, nó cũng xóa các vùng hài hước , đây là khuyết điểm .


Bây giờ bạn có thể thấy rằng jdk-13không chờ rác thải chất đống ở khu vực cũ khi các đối tượng thực sự lớn được phân bổ, nhưng kích hoạt đồng thời chu trình GC , đã lưu lại ngày; không giống như jdk-8.

Bạn có thể muốn đọc những gì DisableExplicitGCvà / hoặc ExplicitGCInvokesConcurrentcó nghĩa, cùng với System.gcvà hiểu lý do tại sao gọi System.gcthực sự giúp đỡ ở đây.


1
Java 8 không sử dụng G1GC theo mặc định. Và nhật ký GC của OP cũng cho thấy rõ rằng nó đang sử dụng song song GC cho thế hệ cũ. Và đối với một nhà sưu tập không đồng thời như vậy, nó đơn giản như được mô tả trong câu trả lời này
Holger

@Holger Tôi đã xem lại câu trả lời này vào sáng hôm nay chỉ để nhận ra rằng đó thực sự là ParalleGC, tôi đã chỉnh sửa và xin lỗi (và cảm ơn bạn) vì đã chứng minh tôi sai.
Eugene

1
Sự phân bổ hài hước của người Viking vẫn là một gợi ý chính xác. Với một trình thu thập không đồng thời, nó ngụ ý rằng GC đầu tiên sẽ chạy khi thế hệ cũ đầy, do đó, việc không lấy lại đủ không gian sẽ khiến nó gây tử vong. Ngược lại, khi bạn giảm kích thước mảng, một GC trẻ sẽ được kích hoạt khi vẫn còn bộ nhớ trong thế hệ cũ, do đó trình thu thập có thể thúc đẩy các đối tượng và tiếp tục. Mặt khác, đối với một trình thu thập đồng thời, việc kích hoạt gc trước khi hết heap là hết, vì vậy -XX:+UseG1GChãy làm cho nó hoạt động trong Java 8, giống như -XX:+UseParallelOldGClàm cho nó thất bại trong các JVM mới.
Holger
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.