强引用、软引用、弱引用、虚引用
Java有不同的引用类型,分别是:强引用、软引用、弱引用、虚引用,不同的引用类型跟我们的垃圾回收也有着不同的规则。
强引用
我们直接通过new关键字创建出来的对象都叫强引用对象,比如:
Object obj = new Object();
强引用的特点:
- 强引用可以直接访问目标对象。
- 强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象。
- 强引用可能导致内存泄漏。
软引用
软引用是除了强引用外,最强的引用类型。可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收(只有当JVM认为内存不足时,才会试图去回收软引用的对象,JVM 会确保在抛出OutOfMemoryError 之前,清理软引用指向的对象 )。因此,软引用可以用于实现对内存敏感的高速缓存。
代码示例:
User user = new User();
SoftReference softReference = new SoftReference<>(user);
user = null;//销毁强引用
System.gc();//手动垃圾回收
System.out.println(softReference.get());//打印软引用中user对象地址值:demo2.User@b4c966a
注意:触发软引用回收的点在于当内存空间已经装不下的时候以及内存空间很紧张的时候执行回收。
代码示例:
/**
* @Description: 需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M
*/
public class Test {
private static List
打印结果:
null
null
null
null
null
null
null
null
null
[B@10f87f48
弱引用
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
我们以同样的方式来测试弱引用:
private static void testWeakReference() {
for (int i = 0; i < 10; i++) {
byte[] buff = new byte[1024 * 1024];
WeakReference sr = new WeakReference<>(buff);
list.add(sr);
}
System.gc(); //主动通知垃圾回收
for(int i=0; i < list.size(); i++){
Object obj = ((WeakReference) list.get(i)).get();
System.out.println(obj);
}
}
打印结果如下:
null
null
null
null
null
null
null
null
null
null
可以发现所有被弱引用关联的对象都被垃圾回收了。
虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。虚引用在创建时必须传入一个引用队列作为参数,当垃圾收集器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
public class PhantomReference extends Reference {
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue super T> q) {
super(referent, q);
}
}
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
【生存还是死亡?】对象的finalization机制
现在理解了GC Roots和引用类型的概念后,也就知道了哪些对象可以被回收,哪些对象不能回收。
有GC Roots引用的对象不能回收,没有GC Roots引用的对象可以回收,如果有GC Roots引用,但是如果是软引用或者弱引用的,也有可能被回收掉。
问:假设没有GC Roots引用的对象,是一定立马被回收吗?
答:其实不是的,这里有一个 finalize()方法可以拯救他自己
示例代码:
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次, 因为一个对象的finalize()方法最多只会被系统自动调用一次
*/
public class TestFinalize {
public static TestFinalize testFinalize;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("当前类的finalize方法执行");
testFinalize = this;
}
public static void main(String[] args) {
testFinalize = new TestFinalize();
//对象第一次成功拯救自己
testFinalize = null;
System.gc();
System.out.println("第一次 gc......");
try {
//因为Finalizer方法优先级很低, 暂停0.5秒, 以等待它
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(testFinalize == null){
System.out.println("对象已死");
}else{
System.out.println("对象依然存活");
}
// 下面这段代码与上面的完全相同, 但是这次自救却失败了
testFinalize = null;
System.gc();
System.out.println("第二次 gc......");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(testFinalize == null){
System.out.println("对象已死");
}else{
System.out.println("对象依然存活");
}
}
}
打印结果:
第一次 gc......
当前类的finalize方法执行
对象依然存活
第二次 gc......
对象已死
注意:finalize方法只会被调用一次!
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
- 在 finalize()时可能会导致对象复活
- finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,则 finalize()方法将没有执行机会
- 一个糟糕的fnalize()会严重影响Gc的性能
从功能上来说, finalize()方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以 finalize()方法在本质上不同于c++中的析构函数。
其次它的运行代价高昂, 不确定性大, 无法保证各个对象的调用顺序, 如今已被官方明确声明为不推荐使用的语法。有些教材中描述它适合做“关闭外部资源”之类的清理性工作, 这完全是对finalize()方法用途的一种自我安慰。finalize()能做的所有工作, 使用try-finally或者其他方式都可以做得更好、更及时, 所以笔者建议大家完全可以忘掉Java语言里面的这个方法。
小结
到这儿已经把为什么要回收垃圾,以及哪些对象会被回收,哪些对象不会被回收的情况给大家介绍清楚了。接下来我们就要正式进入垃圾回收的算法以及内存分配策略的学习了。