超過 GC 回收上限,表示 Java 程序花費 98% 以上的時間執行 GC,但只恢復了不到 2% 的記憶體,且執行連續重複了 5 次,就會產生 Java OutOfMemoryError 的錯誤,模擬建立一個物件,將其加入到一個 List 中,大小限制為 1000 個,重複建立過程,直到 JVM 拋出錯誤, Java 147 增加了範例,並透過單元測試來驗證產出結果。
Table of Contents
ToggleJava OutOfMemoryError
GC Overhead Limit Exceeded
超過 GC 回收上限,設定 JVM Arguments。
-Xmn30m -Xms120m -Xmx120m
檔案目錄
./
+- src
+- test
| +- org
| +- ruoxue
| +- java_147
| +- memory
| +- GCOverheadLimitExceededTest.java
Out of memory
當引用大量物件沒有被釋放,JVM 無法對其自動回收,常見於使用了文件等資源沒有回收等。
測試 JUnit 4
list
建立一個物件,將其加入到一個 List 中,大小限制為 1000 個,重複建立過程。
public static class Key {
private Integer id;
public Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
@Test
public void list() {
List<Key> list = new ArrayList<Key>();
int counter = 1;
for (;;) {
for (int i = 0; i < 1000; i++) {
list.add(new Key(i));
}
Runtime rt = Runtime.getRuntime();
System.out.printf("[%d] free memory: %s%n", counter++, rt.freeMemory());
System.out.println(list.size());
}
}
[3362] free memory: 4132688
3362000
[3363] free memory: 4132688
3363000
[3364] free memory: 4132688
3364000
[3365] free memory: 4132688
3365000
[3366] free memory: 4132688
3366000
[Full GC (Ergonomics) 115235K->115235K(119296K), 0.9471333 secs]
[Full GC (Ergonomics) 115236K->115236K(119296K), 0.9147213 secs]
[Full GC (Ergonomics) 115237K->115237K(119296K), 0.8869343 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
map
建立一個物件,將其加入到一個 Map 中,大小限制為 1000 個,重複建立過程。
@Test
public void map() {
Map<Key, Integer> map = new HashMap<Key, Integer>();
int counter = 1;
for (;;) {
for (int i = 0; i < 1000; i++) {
map.put(new Key(i), i);
}
Runtime rt = Runtime.getRuntime();
System.out.printf("[%d] free memory: %s%n", counter++, rt.freeMemory());
System.out.println(map.size());
}
}
[1086] free memory: 4272040
1086000
[1087] free memory: 4116664
1087000
[1088] free memory: 4116664
1088000
[1089] free memory: 4116664
1089000
[1090] free memory: 4116664
1090000
[Full GC (Ergonomics) 115274K->115274K(119296K), 0.0707265 secs]
[Full GC (Ergonomics) 115275K->115275K(119296K), 0.0706493 secs]
[Full GC (Ergonomics) 115277K->115277K(119296K), 0.0702664 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
byteArray
建立一個 Byte Array,將其加入到一個 Map 中,大小限制為 1000 個,重複建立過程。
@Test
public void byteArray() {
Map<Byte[], Integer> map = new HashMap<Byte[], Integer>();
int counter = 1;
for (;;) {
for (int i = 0; i < 1000; i++) {
map.put(new Byte[10], i);
}
Runtime rt = Runtime.getRuntime();
System.out.printf("[%d] free memory: %s%n", counter++, rt.freeMemory());
System.out.println(map.size());
}
}
[1064] free memory: 4086008
1064000
[1065] free memory: 4086008
1065000
[1066] free memory: 4086008
1066000
[1067] free memory: 4086008
1067000
[1068] free memory: 4086008
1068000
[Full GC (Ergonomics) 115346K->115290K(119296K), 0.3285218 secs]
[Full GC (Ergonomics) 115292K->115292K(119296K), 0.2679968 secs]
[Full GC (Ergonomics) 115294K->115294K(119296K), 0.2674329 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
normal
建立一個物件,將其加入到一個 Map 中,大小限制為 1000 個,重複建立過程,觸發 GC 回收,記憶體一直保持在可用空間,並不會產生記憶體不足的錯誤。
public static class KeyEquals {
Integer id;
public KeyEquals(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
public boolean equals(Object object) {
if (!(object instanceof KeyEquals)) {
return false;
}
if (this == object) {
return true;
}
KeyEquals other = (KeyEquals) object;
return id.equals(other.id);
}
}
@Test
public void normal() {
Map<KeyEquals, Integer> map = new HashMap<KeyEquals, Integer>();
int counter = 1;
for (;;) {
for (int i = 0; i < 1000; i++) {
map.put(new KeyEquals(i), i);
}
Runtime rt = Runtime.getRuntime();
System.out.printf("[%d] free memory: %s%n", counter++, rt.freeMemory());
System.out.println(map.size());
}
}
[1236275] free memory: 122525768
1000
[1236276] free memory: 122525768
1000
[1236277] free memory: 122525768
1000
[1236278] free memory: 122525768
1000
[1236279] free memory: 122525768
1000
GCOverheadLimitExceededTest.java
package org.ruoxue.java_147.memory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
public class GCOverheadLimitExceededTest {
public static class Key {
private Integer id;
public Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static class KeyEquals {
Integer id;
public KeyEquals(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
public boolean equals(Object object) {
if (!(object instanceof KeyEquals)) {
return false;
}
if (this == object) {
return true;
}
KeyEquals other = (KeyEquals) object;
return id.equals(other.id);
}
}
@Test
public void list() {
List<Key> list = new ArrayList<Key>();
int counter = 1;
for (;;) {
for (int i = 0; i < 1000; i++) {
list.add(new Key(i));
}
Runtime rt = Runtime.getRuntime();
System.out.printf("[%d] free memory: %s%n", counter++, rt.freeMemory());
System.out.println(list.size());
}
}
@Test
public void map() {
Map<Key, Integer> map = new HashMap<Key, Integer>();
int counter = 1;
for (;;) {
for (int i = 0; i < 1000; i++) {
map.put(new Key(i), i);
}
Runtime rt = Runtime.getRuntime();
System.out.printf("[%d] free memory: %s%n", counter++, rt.freeMemory());
System.out.println(map.size());
}
}
@Test
public void byteArray() {
Map<Byte[], Integer> map = new HashMap<Byte[], Integer>();
int counter = 1;
for (;;) {
for (int i = 0; i < 1000; i++) {
map.put(new Byte[10], i);
}
Runtime rt = Runtime.getRuntime();
System.out.printf("[%d] free memory: %s%n", counter++, rt.freeMemory());
System.out.println(map.size());
}
}
@Test
public void normal() {
Map<KeyEquals, Integer> map = new HashMap<KeyEquals, Integer>();
int counter = 1;
for (;;) {
for (int i = 0; i < 1000; i++) {
map.put(new KeyEquals(i), i);
}
Runtime rt = Runtime.getRuntime();
System.out.printf("[%d] free memory: %s%n", counter++, rt.freeMemory());
System.out.println(map.size());
}
}
}
心得分享
從輸出結果可以看到,限制 1000 個物件並沒有達到作用,集合的容量遠超過了 1000 ,最後也出現了預期的錯誤,因為類別 Key 只重寫了 hashCode 方法,卻沒有重寫 equals 方法,在使用 containsKey 方法其實就出現了問題,於是就會一直往集合中增加 Key,直至 GC 都無法清除,最終會拋出此錯誤。
Java Heap Space
此外,與 JVM 配置也會有關係,如果設置的 Head Space 特別小,會直接拋出此錯誤,所以有時在資源受限的情況下,無法準確預測程式會拋出哪種具體的原因,參考以下情況作進一步調整:
- 增加 JVM Arguments -XX:-UseGCOverheadLimit 但這樣並沒有真正解決問題,只是延遲錯誤發生。
- 檢查程式中是否有大量的無窮迴圈或有使用佔用大記憶體的程式,應該優化程式語法
- dump 記憶體分析,檢查是否存在 Memory Leak ,如果沒有的話,可以加大記憶體設置。