在 Java 的对象世界里面,对象的引用有 4 类之分,分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。
在使用
new
操作符创建的对象,在对象的生命周期内,GC 是不会回收该对象的,除非主动释放该对象(将引用设置为 null),否则 JVM 宁可抛出 OOM 异常,也不会将具有强引用的对象回收。
如下代码所示,将虚拟机的青年带设置为 10M,老年代设置为 20M,创建两个大对象,大小都为 15 M。
package org.gettingreal.jvm.learning.references;
/**
* 强引用测试
*/
public class StrongReferenceTest {
public static void main(String[] args) throws Exception {
// 大小定位 15 M
int size = 15 * 1024 * 1024;
// 创建第一个对象
byte[] firstBigObject = new byte[size];
// 创建第二个对象
byte[] secondBigObject = new byte[size];
}
}
将代码编译后,执行如下命令来观察结果。
# 执行编译
mvn package -DskipTests
# 运行 StrongReferenceTest
java -XX:+PrintGC -Xmn10m -Xms30m -Xmx30m -classpath target/jvm-learnging-1.0.0-SNAPSHOT.jar org.gettingreal.jvm.learning.references.StrongReferenceTest
# 输出
[GC (Allocation Failure) 16195K->15960K(29696K), 0.0012633 secs]
[GC (Allocation Failure) 15960K->15944K(29696K), 0.0007246 secs]
[Full GC (Allocation Failure) 15944K->15672K(29696K), 0.0033753 secs]
[GC (Allocation Failure) 15672K->15672K(29696K), 0.0005597 secs]
[Full GC (Allocation Failure) 15672K->15659K(29696K), 0.0027816 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at org.gettingreal.jvm.learning.references.StrongReferenceTest.main(StrongReferenceTest.java:16)
可以看出 JVM 执行多次 GC 操作,发现最终无法回收强引用的对象,抛出 OOM 异常。JVM 内部执行的步骤大体如下:
软引用表示一个对象有用,但是非必需的,意思是如果一个对象是软引用的,在内存空间充足的情况下,JVM 不会回收该对象。然后在内存空间不足时,JVM 会将该对象回收。
如下代码所示,测试软引用回收的情况,将虚拟机的青年带设置为 10M,老年代设置为 20M,创建两个大对象,大小都为 15 M。
package org.gettingreal.jvm.learning.references;
import java.lang.ref.SoftReference;
/**
* 软引用测试
*/
public class SoftReferenceTest {
public static void main(String[] args) throws Exception {
// 大小定位 15 M
int size = 15 * 1024 * 1024;
// 创建第一个对象
byte[] firstBigObject = new byte[size];
// 将第一个对象加入到软引用对象中
SoftReference softReference1 = new SoftReference(firstBigObject);
// 需要手动释放掉强引用
firstBigObject = null;
// 创建第二个对象
byte[] secondBigObject = new byte[size];
// 将第一个对象加入到软引用对象中
SoftReference softReference2 = new SoftReference(secondBigObject);
// 需要手动释放掉强引用
secondBigObject = null;
}
}
将代码编译后,执行如下命令来观察结果。
# 执行编译
mvn package -DskipTests
# 运行 SoftReferenceTest
java -XX:+PrintGC -Xmn10m -Xms30m -Xmx30m -classpath target/jvm-learnging-1.0.0-SNAPSHOT.jar org.gettingreal.jvm.learning.references.SoftReferenceTest
# 输出
[GC (Allocation Failure) 16196K->15960K(29696K), 0.0006832 secs]
[GC (Allocation Failure) 15960K->15888K(29696K), 0.0006921 secs]
[Full GC (Allocation Failure) 15888K->15672K(29696K), 0.0031219 secs]
[GC (Allocation Failure) 15672K->15672K(29696K), 0.0003450 secs]
[Full GC (Allocation Failure) 15672K->299K(29696K), 0.0022771 secs]
可以观察到并没有发生 OOM 异常,说明 JVM 回收掉第一个大对象。
可以通过ReferenceQueue
来观察引用被回收的情况,只要这个引用被回收,就会将引用添加到此队列中。
package org.gettingreal.jvm.learning.references;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
/**
* 软引用队列测试
*/
public class SoftReferenceQueueTest {
public static void main(String[] args) throws Exception {
// 大小定位 15 M
int size = 15 * 1024 * 1024;
// 定义队列
ReferenceQueue referenceQueue = new ReferenceQueue();
try {
// 创建第一个对象
byte[] firstBigObject = new byte[size];
// 将第一个对象加入到软引用对象中
SoftReference softReference1 = new SoftReference(firstBigObject, referenceQueue);
System.out.println("添加软引用:" + softReference1);
// 需要手动释放掉强引用
firstBigObject = null;
// 创建第二个对象
byte[] secondBigObject = new byte[size];
// 将第一个对象加入到软引用对象中
SoftReference softReference2 = new SoftReference(secondBigObject, referenceQueue);
System.out.println("添加软引用:" + softReference2);
// 需要手动释放掉强引用
secondBigObject = null;
} finally {
SoftReference softReference = null;
while ((softReference = (SoftReference) referenceQueue.poll()) != null) {
System.out.println("回收软引用:" + softReference);
}
}
}
}
将代码编译后,执行如下命令来观察结果。
# 执行编译
mvn package -DskipTests
# 运行 SoftReferenceQueueTest
java -Xmn10m -Xms30m -Xmx30m -classpath target/jvm-learnging-1.0.0-SNAPSHOT.jar org.gettingreal.jvm.learning.references.SoftReferenceQueueTest
# 输出
添加软引用:java.lang.ref.SoftReference@3d4eac69
[GC (Allocation Failure) 16197K->15976K(29696K), 0.0009770 secs]
[GC (Allocation Failure) 15976K->15912K(29696K), 0.0005638 secs]
[Full GC (Allocation Failure) 15912K->15674K(29696K), 0.0039520 secs]
[GC (Allocation Failure) 15674K->15674K(29696K), 0.0004479 secs]
[Full GC (Allocation Failure) 15674K->301K(29696K), 0.0026771 secs]
添加软引用:java.lang.ref.SoftReference@42a57993
回收软引用:java.lang.ref.SoftReference@3d4eac69
可以看出,第一个软引用被回收了。
类似于软引用,但是比软引用还要弱一些,用来描述非必需的对象,只要发生 GC 操作,弱引用都会被回收。
如下代码所示,测试弱引用回收的情况,将虚拟机的青年带设置为 10M,老年代设置为 20M,创建两个大对象,大小都为 15 M。
package org.gettingreal.jvm.learning.references;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
/**
* 弱引用测试
*/
public class WeakReferenceTest {
public static void main(String[] args) throws Exception {
// 大小定位 15 M
int size = 15 * 1024 * 1024;
// 定义队列
ReferenceQueue referenceQueue = new ReferenceQueue();
try {
// 创建第一个对象
byte[] firstBigObject = new byte[size];
// 将第一个对象加入到弱引用对象中
WeakReference weakReference = new WeakReference(firstBigObject, referenceQueue);
System.out.println("添加弱引用:" + weakReference);
// 需要手动释放掉强引用
firstBigObject = null;
// 创建第二个对象
byte[] secondBigObject = new byte[size];
// 将第一个对象加入到弱引用对象中
WeakReference weakReference2 = new WeakReference(secondBigObject, referenceQueue);
System.out.println("添加弱引用:" + weakReference2);
// 需要手动释放掉强引用
secondBigObject = null;
// 执行一次 gc 操作
System.gc();
} finally {
WeakReference wr = null;
while ((wr = (WeakReference) referenceQueue.poll()) != null) {
System.out.println("回收弱引用:" + wr);
}
}
}
}
将代码编译后,执行如下命令来观察结果。
# 执行编译
mvn package -DskipTests
# 运行 WeakReferenceTest
java -XX:+PrintGC -Xmn10m -Xms30m -Xmx30m -classpath target/jvm-learnging-1.0.0-SNAPSHOT.jar org.gettingreal.jvm.learning.references.WeakReferenceTest
# 输出
添加弱引用:java.lang.ref.WeakReference@3d4eac69
[GC (Allocation Failure) 16197K->15960K(29696K), 0.0008933 secs]
[GC (Allocation Failure) 15960K->15928K(29696K), 0.0008108 secs]
[Full GC (Allocation Failure) 15928K->314K(29696K), 0.0029523 secs]
添加弱引用:java.lang.ref.WeakReference@42a57993
[GC (System.gc()) 15838K->15706K(29696K), 0.0004367 secs]
[Full GC (System.gc()) 15706K->308K(29696K), 0.0023688 secs]
回收弱引用:java.lang.ref.WeakReference@42a57993
回收弱引用:java.lang.ref.WeakReference@3d4eac69
可以发现,只要发生 GC 操作,弱引用都将会被回收。
虚引用实际上是为了资源释放的细粒度控制,但是使用虚引用却需要小心,因为虚引用可能会导致 OOM 的发生。虚引用更倾向于实现程序员对内存回收的细粒度控制,当虚引用会被回收时,向引用系统发出通知,此时可以执行内存的释放相关操作。
虚引用不会主动释放其指向的对象的内存区域,所以当内存满时,会导致 OOM 异常的发生。将虚拟机的青年带设置为 10M,老年代设置为 20M,创建两个大对象,大小都为 15 M。如下代码所示:
package org.gettingreal.jvm.learning.references;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
/**
* 虚引用测试
*/
public class PhantomReferenceTest {
public static void main(String[] args) {
int size = 15 * 1024 * 1024;
ReferenceQueue referenceQueue = null;
try {
// 定义队列
referenceQueue = new ReferenceQueue();
// 将第一个对象加入到软虚用对象中
PhantomReference phantomReference1 = new PhantomReference(new byte[size], referenceQueue);
System.out.println("添加虚引用,phantomReference1:" + phantomReference1);
// 将第一个对象加入到软引用对象中
PhantomReference phantomReference2 = new PhantomReference(new byte[size], referenceQueue);
System.out.println("添加虚引用,phantomReference2:" + phantomReference2);
System.gc();
} finally {
System.out.println("\n检查回收虚引用:");
PhantomReference pr = null;
while ((pr = (PhantomReference) referenceQueue.poll()) != null) {
System.out.println("回收虚引用:" + pr);
}
}
}
}
将代码编译后,执行如下命令来观察结果。
# 执行编译
mvn package -DskipTests
# 运行 PhantomReferenceTest
java -XX:+PrintGC -Xmn10m -Xms30m -Xmx30m -classpath target/jvm-learnging-1.0.0-SNAPSHOT.jar org.gettingreal.jvm.learning.references.PhantomReferenceTest
# 输出
添加虚引用,phantomReference1:java.lang.ref.PhantomReference@3d4eac69
[GC (Allocation Failure) 16197K->15960K(29696K), 0.0006786 secs]
[GC (Allocation Failure) 15960K->15896K(29696K), 0.0007171 secs]
[Full GC (Allocation Failure) 15896K->15674K(29696K), 0.0032693 secs]
[GC (Allocation Failure) 15674K->15674K(29696K), 0.0005009 secs]
[Full GC (Allocation Failure) 15674K->15661K(29696K), 0.0024825 secs]
检查回收虚引用:
回收虚引用:java.lang.ref.PhantomReference@3d4eac69
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at org.gettingreal.jvm.learning.references.PhantomReferenceTest.main(PhantomReferenceTest.java:24)
可以看出并没有释放 phantomReference1
对应的内存。可以通过手动调用 PhantomReference 实例的 clear() 方法来释放对应的内存。
package org.gettingreal.jvm.learning.references;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
/**
* 虚引用清理测试
*/
public class PhantomReferenceCleanTest {
public static void main(String[] args) throws Exception {
int size = 15 * 1024 * 1024;
ReferenceQueue referenceQueue = null;
try {
// 定义队列
referenceQueue = new ReferenceQueue();
// 将第一个对象加入到软虚用对象中
PhantomReference phantomReference = new PhantomReference(new byte[size], referenceQueue);
System.out.println("添加虚引用,phantomReference1:" + phantomReference);
System.gc();
} finally {
System.out.println("\n检查回收虚引用:");
PhantomReference pr = null;
while ((pr = (PhantomReference) referenceQueue.poll()) != null) {
System.out.println("回收虚引用:" + pr);
// 调用 clear() 方法,模拟内存释放的细粒度操作
pr.clear();
}
}
try {
Thread.sleep(3000);
// 将第二个对象加入到软虚用对象中
PhantomReference phantomReference2 = new PhantomReference(new byte[size], referenceQueue);
System.out.println("添加虚引用,phantomReference2:" + phantomReference2);
System.gc();
} finally {
System.out.println("\n检查回收虚引用2:");
PhantomReference pr = null;
while ((pr = (PhantomReference) referenceQueue.poll()) != null) {
System.out.println("回收虚引用:" + pr);
// 调用 clear() 方法,模拟内存释放的细粒度操作
pr.clear();
}
}
}
}
将代码编译后,执行如下命令来观察结果。
# 执行编译
mvn package -DskipTests
# 运行 PhantomReferenceCleanTest
java -XX:+PrintGC -Xmn10m -Xms30m -Xmx30m -classpath target/jvm-learnging-1.0.0-SNAPSHOT.jar org.gettingreal.jvm.learning.references.PhantomReferenceCleanTest
# 输出
添加虚引用,phantomReference1:java.lang.ref.PhantomReference@3d4eac69
[GC (System.gc()) 16197K->15992K(29696K), 0.0009614 secs]
[Full GC (System.gc()) 15992K->15675K(29696K), 0.0032103 secs]
检查回收虚引用:
回收虚引用:java.lang.ref.PhantomReference@3d4eac69
[GC (Allocation Failure) 15839K->15739K(29696K), 0.0012892 secs]
[GC (Allocation Failure) 15739K->15771K(29696K), 0.0007469 secs]
[Full GC (Allocation Failure) 15771K->308K(29696K), 0.0042175 secs]
添加虚引用,phantomReference2:java.lang.ref.PhantomReference@42a57993
[GC (System.gc()) 15832K->15700K(29696K), 0.0004901 secs]
[Full GC (System.gc()) 15700K->15669K(29696K), 0.0018866 secs]
检查回收虚引用2:
回收虚引用:java.lang.ref.PhantomReference@42a57993
可以发现,phantomReference1 在 GC 操作时,加入到了 referenceQueue 队列,在遍历队列时,调用引用的 clean() 方法来执行对象对应内存的释放。再次添加一个对象时,不会出现 OOM 异常。