個人博客
淺談Java中的軟引用
前言
Java中有四種引用類型:強引用、軟引用、弱引用、虛引用。四種引用類型分別有不同的應用場景,本文主要演示軟引用的簡單使用、可能遇到的問題以及對應的解決方法。
軟引用的簡單使用
軟引用的特點是:如果一個對象只存在軟引用,那么當內存不足時,GC就會回收這個對象。
設置JVM的最大內存
為了模擬內存不足,這里通過-Xmx來設置JVM的最大可分配內存。
-Xmx100m
這里是使用IntelliJ IDEA來創建項目的。在Run-Edit Configurations中打開
先輸出JVM當前的內存信息
private static void showInitialMemoryInfo() {
MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
System.out.println("最大可用內存:" + toMB(mbean.getHeapMemoryUsage().getMax()));
for (MemoryPoolMXBean mxBean : ManagementFactory.getMemoryPoolMXBeans()) {
System.out.println("Name:" + mxBean.getName()
+ ",Type:" + mxBean.getType()
+ ",Size:" + toMB(mxBean.getUsage().getMax()));
}
}
如果是使用CMD的方式,可以通過javac -encoding utf-8 Main.java先編譯,再通過java -Xmx100m Main來執行
運行結果:
最大可用內存:96.00 M
Name:Code Cache,Type:Non-heap memory,Size:240.00 M
Name:Metaspace,Type:Non-heap memory,Size:-0.00 M
Name:Compressed Class Space,Type:Non-heap memory,Size:1024.00 M
Name:PS Eden Space,Type:Heap memory,Size:25.00 M
Name:PS Survivor Space,Type:Heap memory,Size:4.00 M
Name:PS Old Gen,Type:Heap memory,Size:67.00 M
可以看到,雖然我們指定了100M的內存,但是實際上可分配的內存只有96M。這是因為,內存區域按照新生代:老年代=1:2划分,老年代的大小為67M,新生代又分為Eden區+Survivor From+Survivor To,比例約為8:1:1,由於Survivor From區域和Survivor To區域的大小是相同的,而且這兩個區域同時只會使用一個,因此相當於有一個Survivor的空間是無法使用到的,因此最終可使用的大小為100-4=96M
軟引用的使用
為了演示軟引用,首先創建一個類
static class SoftObject {
byte[] data = new byte[50 * 1024 * 1024];
}
這個類占用內存大小為50M
private static void softReference() {
printSplitLine();
//創建軟引用對象
SoftReference<SoftObject> reference = new SoftReference<>(new SoftObject());
System.out.println(getCurrentMemoryInfo() + ",當前軟引用對象:" + reference.get());
printSplitLine();
//創建強引用對象
SoftObject object = new SoftObject();
printSplitLine();
System.out.println(getCurrentMemoryInfo() + ",當前軟引用對象:" + reference.get());
//創建強引用對象
SoftObject object2 = new SoftObject();
printSplitLine();
System.out.println(getCurrentMemoryInfo() + ",當前軟引用對象:" + reference.get());
}
在這個方法中,先創建了一個只有軟引用的對象,然后打印了該對象的地址。然后又創建了一個強引用對象,由於此時內存不足以創建新的對象,因此會回收之前創建的只有軟引用的對象。然后再次創建強引用對象,由於此時已經無法分配足夠的空間來創建對象,因此會拋出OOM的異常。
為了方便看到GC的信息,這里加上了-XX:PrintGC
輸出結果
========================================================================
當前最大可用內存:96.00 M,當前空閑內存:42.48 M,當前軟引用對象:Main$SoftObject@5cad8086
========================================================================
[GC (Allocation Failure) 54806K->52176K(98304K), 0.0015117 secs]
[GC (Allocation Failure) 52176K->52160K(98304K), 0.0015483 secs]
[Full GC (Allocation Failure) 52160K->52073K(98304K), 0.0096901 secs]
[GC (Allocation Failure) 52073K->52073K(98304K), 0.0009906 secs]
[Full GC (Allocation Failure) 52073K->836K(77824K), 0.0088653 secs]
========================================================================
當前最大可用內存:96.00 M,當前空閑內存:44.18 M,當前軟引用對象:null
[GC (Allocation Failure) 53060K->52076K(98304K), 0.0007633 secs]
[GC (Allocation Failure) 52076K->52140K(94720K), 0.0013708 secs]
[Full GC (Allocation Failure) 52140K->52039K(94720K), 0.0218573 secs]
[GC (Allocation Failure) 52039K->52039K(97280K), 0.0006524 secs]
[Full GC (Allocation Failure) 52039K->52031K(97280K), 0.0042017 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Main$SoftObject.<init>(Main.java:113)
at Main.softReference(Main.java:34)
at Main.main(Main.java:13)
使用軟引用可能存在的問題
目前來看,一切都是正常的。看下以下的代碼
static class SmallSoftObject {
byte[] data = new byte[1024];
}
private static void softReferenceOverHeadLimit() {
int capacity = 1024 * 1024;
HashSet<SoftReference<SmallSoftObject>> set = new HashSet<>(capacity);
for (int i = 0; i < capacity; i++) {
set.add(new SoftReference<>(new SmallSoftObject()));
}
System.out.println("End");
}
為了演示這種異常,先通過-Xmx40m,將JVM的最大內存改為40M,然后創建的對象占用內存也改為1kb。通過一個HashSet來引用SoftReference對象。運行后,輸出結果如下:
//省略部分輸出
[Full GC (Ergonomics) 32742K->32697K(36864K), 0.1614924 secs]
[Full GC (Ergonomics) 32742K->32700K(36864K), 0.1674457 secs]
[Full GC (Ergonomics) 32743K->32703K(36864K), 0.1609792 secs]
[Full GC (Ergonomics) 32742K->32705K(36864K), 0.1612034 secs]
[Full GC (Ergonomics) 32742K->32708K(36864K), 0.1645191 secs]
[Full GC (Ergonomics) 32742K->32710K(36864K), 0.1633229 secs]
[Full GC (Ergonomics) 32743K->32712K(36864K), 0.1632551 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at Main$SmallSoftObject.<init>(Main.java:117)
at Main.softReferenceOverHeadLimit(Main.java:46)
at Main.main(Main.java:13)
可以看到,不同於上面的異常,這里是拋出的overhead limit的OOM異常。拋出這個異常是因為,默認情況下,如果GC花費的時間超過98%,並且GC回收的內存少於2%,JVM就會拋出這個異常。
解決方法
通過引用隊列,在觸發GC時,手動移除HashSet中的SoftReference對象。
private static void softReferenceOverHeadLimitResolve() {
int capacity = 1024 * 1024;
HashSet<SoftReference<SmallSoftObject>> set = new HashSet<>(capacity);
ReferenceQueue<SmallSoftObject> referenceQueue = new ReferenceQueue<>();
for (int i = 0; i < capacity; i++) {
set.add(new SoftReference<>(new SmallSoftObject(), referenceQueue));
removeObject(set, referenceQueue);
}
System.out.println("End");
}
private static void removeObject(HashSet<SoftReference<SmallSoftObject>> set, ReferenceQueue<SmallSoftObject> referenceQueue) {
Reference<? extends SmallSoftObject> poll = referenceQueue.poll();
while (poll != null) {
set.remove(poll);
poll = referenceQueue.poll();
}
}
執行結果
//省略部分輸出
[Full GC (Ergonomics) 32698K->32698K(36864K), 0.0309117 secs]
[Full GC (Ergonomics) 32716K->32716K(36864K), 0.0309336 secs]
[Full GC (Ergonomics) 32734K->32734K(36864K), 0.0323875 secs]
[Full GC (Ergonomics) 32751K->32751K(36864K), 0.0311011 secs]
[Full GC (Ergonomics) 32767K->32767K(36864K), 0.0309406 secs]
[Full GC (Allocation Failure) 32767K->6943K(35840K), 0.0409029 secs]
[GC (Allocation Failure) 12066K->12042K(35840K), 0.0036551 secs]
[GC (Allocation Failure) 17162K->17298K(35840K), 0.0038763 secs]
End
完整代碼
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
showInitialMemoryInfo();
softReferenceOverHeadLimitResolve();
}
/**
* 演示軟引用
*/
private static void softReference() {
printSplitLine();
//創建軟引用對象
SoftReference<SoftObject> reference = new SoftReference<>(new SoftObject());
System.out.println(getCurrentMemoryInfo() + ",當前軟引用對象:" + reference.get());
printSplitLine();
//創建強引用對象
SoftObject object = new SoftObject();
printSplitLine();
System.out.println(getCurrentMemoryInfo() + ",當前軟引用對象:" + reference.get());
//創建強引用對象
SoftObject object2 = new SoftObject();
printSplitLine();
System.out.println(getCurrentMemoryInfo() + ",當前軟引用對象:" + reference.get());
}
/**
* 演示軟引用溢出
*/
private static void softReferenceOverHeadLimit() {
int capacity = 1024 * 1024;
HashSet<SoftReference<SmallSoftObject>> set = new HashSet<>(capacity);
for (int i = 0; i < capacity; i++) {
set.add(new SoftReference<>(new SmallSoftObject()));
}
System.out.println("End");
}
/**
* 演示軟引用溢出解決方案
*/
private static void softReferenceOverHeadLimitResolve() {
int capacity = 1024 * 1024;
HashSet<SoftReference<SmallSoftObject>> set = new HashSet<>(capacity);
ReferenceQueue<SmallSoftObject> referenceQueue = new ReferenceQueue<>();
for (int i = 0; i < capacity; i++) {
set.add(new SoftReference<>(new SmallSoftObject(), referenceQueue));
removeObject(set, referenceQueue);
}
System.out.println("End");
}
private static void removeObject(HashSet<SoftReference<SmallSoftObject>> set, ReferenceQueue<SmallSoftObject> referenceQueue) {
Reference<? extends SmallSoftObject> poll = referenceQueue.poll();
while (poll != null) {
set.remove(poll);
poll = referenceQueue.poll();
}
}
/**
* 顯示初始的內存信息
*/
private static void showInitialMemoryInfo() {
MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
System.out.println("最大可用內存:" + toMB(mbean.getHeapMemoryUsage().getMax()));
for (MemoryPoolMXBean mxBean : ManagementFactory.getMemoryPoolMXBeans()) {
System.out.println("Name:" + mxBean.getName()
+ ",Type:" + mxBean.getType()
+ ",Size:" + toMB(mxBean.getUsage().getMax()));
}
}
/**
* 獲取當前內存信息
*
* @return
*/
private static String getCurrentMemoryInfo() {
return "當前最大可用內存:" + toMB(Runtime.getRuntime().maxMemory()) + ",當前空閑內存:" + toMB(Runtime.getRuntime().freeMemory());
}
/**
* 將字節轉換成MB的格式
*
* @param b
* @return
*/
private static String toMB(Long b) {
return String.format("%.2f M", (double) b / (1024 * 1024));
}
/**
* 打印分隔行
*/
private static void printSplitLine() {
System.out.println("========================================================================");
}
static class SoftObject {
byte[] data = new byte[50 * 1024 * 1024];
}
static class SmallSoftObject {
byte[] data = new byte[1024];
}
}
結束語
軟引用在開發中可能會常用到,了解它的基本用法及可能出現的異常都是有必要的,因此特記錄於此。如有表述有誤的地方,歡迎各位大佬留言指正,感謝!
參考
Android工程師進階34講-第02講:GC回收機制與分代回收策略