1.Java的四种引用
我们平时创建对象直接返回的引用就是所谓的强引用。我们传统的强引用只能给对象两种状态:被引用|不被引用,但是对于下面的几个案例,我们的强引用却很难达到目的
- 对于缓存这类存储空间,我们希望在空间允许的时候将其保留,空间不允许的时间将其进行回收
- 如果我们程序中维护了一个Map<key,value>的引用,但是map中的某个键已经不能在程序的任何地方访问了,我们如何将其进行回收
- 对于堆外分配的内存,我们如何通知操作系统将其进行回收?
2.强引用
强引用是我们平时最常用的引用,只要强引用还指向着某个对象,无论如何该对象都不会被回收,所以强引用是造成内存泄漏的主要原因,其他三种引用都不会造成内存泄漏,因为其他三种引用在特定情况下最终都会被gc回收。
3.软引用
java.lang.ref.SoftReference
软引用,当内存不足时会进行第一次gc,如果gc完成之后内存够用了则不会发生什么特殊的,但是如果第一次gc之后内存依旧不足,那么将会将只有弱引用指向的对象进行回收。
一句话:内存真实不足时会进行回收。
应用场景:缓存等对程序有用但是非必须的对象.(mybatis底层就使用到了软引用,有兴趣的可以去看看)
4.弱引用
java.lang.ref.WeakReference
只有弱引用指向的对象,下一次垃圾回收就会被清理。
应用场景:用于如果没有其他引用指向的对象,那么会直接进行回收。如开头提到的第二个问题。Java.util包下有个WeakHashMap类,这个类中的key值持有的就是弱引用,当我们程序中再也没有对key的其他引用时,下一次垃圾回收会直接将该key所对应的entry进行回收。
5.虚引用
java.lang.ref.PhantomReference
虚引用无法获取到它所指向的对象的强引用。任何时候调用其get()方法返回的都是null值,虚引用需要结合引用队列进行使用,当虚引用所指向的对象被回收时,该虚引用会被加入到引用队列中,相当于通知了系统,该虚引用指向的对象被回收了。
应用场景:堆外内存回收。
6.终结器引用
java.lang.ref.FinalReference
大家看到这个标题可能可能会想,不是四种引用吗,怎么多出来一种,我们的引用父类Reference除了上面三种实现类之外,还有另外一个包访问权限的实现类就是FinalReference终结器引用,该引用是虚拟机底层用来实现gc过程中调用finalize()方法的。
7.finalize方法调用后对象就被回收了吗?
大家平时可能都存在一个误区:对象被回收时,会先调用其finalize方法,如果在finalize方法中对象没有自救成功(在finalize方法没有和任何一个gc roots所对应的引用链建立联系),那么该对象就会被立刻回收。
实际上并不是立刻回收,而是会在下一次垃圾回收时将该对象进行回收。
下面有一个代码可以验证这个事情。
JVM参数:-XX:+PrintGCDetails
public class TestGC {
byte[] bytes = new byte[1024 * 1024 * 20];
public static void main(String[] args) {
TestGC testGC = new TestGC();
testGC = null;
System.gc();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
}
@Override
protected void finalize() throws Throwable {
System.out.println("finalize方法被调用");
}
}
运行结果
我们发现第一次gc时,对应对象的20M空间并没有被回收,而是继续保存在我们的老年代,第二次垃圾回收时才真正进行回收。
finalize方法的底层原理实际上是:
最后在说一个,对象只有真正被回收时,才会被加入到其对应的软、弱、虚引用的引用队列中,如果该对象重写了finalize方法,那么至少要执行两次gc程序才能正常回收并被引用队列捕获,不然如果只进行了一次回收,那么从引用队列获取通知时将会造成程序阻塞。
下面这个代码是关于finalize方法和引用队列的调用顺序,有兴趣的同学可以看一下。
public class PhantomReferenceTest {
static PhantomReference<User> phantomRef;
static ReferenceQueue<User> queue;
static class Damon extends Thread{
@Override
public void run() {
while (true){
if (queue != null){
try {
//对象真正被回收时,其对应的虚引用会被加入到引用队列中
Reference<? extends User> remove = queue.remove();
if (remove != null){
System.out.println("对象真正要被回收了:"+
remove.getClass().getSimpleName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Damon damon = new Damon();
damon.setDaemon(true);
damon.start();
// 开启守护线程
queue = new ReferenceQueue<>();
phantomRef = new PhantomReference<>(new User("方圆"),queue);
System.gc();
System.out.println("第一次GC");
Thread.sleep(1000);
System.gc();
System.out.println("第二次GC");
Thread.sleep(1000);
}
static class User{
String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
@Override
protected void finalize() throws Throwable {
System.out.println("调用了finalize方法");
}
}
}
最后我们强烈不建议大家使用finalize方法,甚至需要尽量避免该方法的重写和使用,使用finally代码快会比重写finalize方法来的更加安全有效。