一、如何判断对象是否存活
判断对象是否存活主要有两种方法。
一是引用计数法。其主要原理为每当一个对象在一个地方被引用,其计数值就+1,引用失效时,计数值-1。
但现在JAVA虚拟机里并没有选择引用计数法来管理内存,因为它有两个弊端:
1.在对象较多时,维护引用计数需要占用大量内存。
2.引用计数法无法解决循环引用的问题。
有关循环引用问题,可以参考下面这段代码
public class ReferenceGC {
public Object instance = null;
public static final int _1MB = 1024*1024;
private byte[] bigSize = new byte[2*_1MB];//仅用作占内存
@Test
public void testGC(){
ReferenceGC objA = new ReferenceGC();
ReferenceGC objB = new ReferenceGC();
objA.instance = objB;//循环引用
objB.instance = objA;
objA = null;
objB = null;
System.gc();//垃圾回收
}
}
结果如下:
[GC (System.gc()) [PSYoungGen: 12704K->3464K(37888K)] 12704K->3472K(123904K), 0.0027792 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 3464K->0K(37888K)] [ParOldGen: 8K->3280K(86016K)] 3472K->3280K(123904K), [Metaspace: 4703K->4703K(1056768K)], 0.0050790 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 37888K, used 655K [0x00000000d6200000, 0x00000000d8c00000, 0x0000000100000000)
eden space 32768K, 2% used [0x00000000d6200000,0x00000000d62a3d98,0x00000000d8200000)
from space 5120K, 0% used [0x00000000d8200000,0x00000000d8200000,0x00000000d8700000)
to space 5120K, 0% used [0x00000000d8700000,0x00000000d8700000,0x00000000d8c00000)
ParOldGen total 86016K, used 3280K [0x0000000082600000, 0x0000000087a00000, 0x00000000d6200000)
object space 86016K, 3% used [0x0000000082600000,0x0000000082934038,0x0000000087a00000)
Metaspace used 4722K, capacity 5130K, committed 5248K, reserved 1056768K
class space used 547K, capacity 562K, committed 640K, reserved 1048576K
FULLGC中新生代有3464K->0K,说明JAVA虚拟机并没有因为循环引用问题而放弃回收objA和objB,从而近一步说明了JAVA虚拟机中用的并不是引用计数法。
二是可达性分析算法。其主要原理是选择一系列GC roots对象作为根节点,从这些结点开始,根据引用关系向下搜索,搜索过程中所走过的路径称为引用链,若某个对象到GC roots之间没有引用链,则认为该对象不可以再次被使用
具体的流程可以参照上图,另外以下几类对象可以被选作GC roots对象
1.JAVA虚拟机栈中本地变量表中引用的对象(e.g. 局部参数,临时变量等)
2.方法区中的类静态属性引用的对象(e.g.静态变量)
3. 方法区中常量引用的对象(e.g.字符串常量池引用的对象)
4.本地方法栈中JNI引用的对象
5.JAVA内部的引用(e.g.基本数据类型对应的Class对象,常驻内存的异常对象,系统的类加载器)
6.所有被同步锁持有的对象
7.JMXBean,JVMTI中注册的回调,本地代码等
JAVA虚拟机使用的是可达性分析算法。
二、四大引用
根据可达性分析算法,判断对象是否存活需要判断对象是否引用链可达,即对象是否被引用。有关引用,有以下几种:
1.强引用。
当内存不足,JVM开始垃圾回收,对于强引用的对象,就是出现了OOM也不会对该对象进行回收。强引用是我们最常见的普通引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾收集器不会碰这种对象。在 Java中最常见的就是强引用,把一个对象赋值给一个引用变量,这个引用变量就是一个强引用,当一个对象被强引用时,它处在可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示的将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了。
Object obj1 = new Object(); // 这样定义的默认就是强引用
Object obj2 = obj1; // obj2引用赋值
obj1 = null; // 置空
System.gc();
System.out.println(obj2); // 正常输出
2.软引用
软引用是一种相对强引用弱化了一些的引用,需要调用 java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说:当系统内存充足时它 不会 被回收,当系统内存不足时它 会 被回收。
软引用通常在对应内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
public class SoftReferenceDemo {
public static void softRef_Memory_Enough(){
Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
System.gc();
System.out.println(o1);
System.out.println(softReference.get());
}
/*
JVM 配置,让它内存不够
-Xms5m -Xmx5m -XX:+PrintGCDetails
*/
public static void softRef_Memory_NotEnough(){
Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
//System.gc();
try {
byte[] bytes = new byte[30*1024*1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(o1);
System.out.println(softReference.get());
}
}
public static void main(String[] args) {
softRef_Memory_NotEnough();
}
}
3.弱引用
弱引用需要用 java.lang.ref.WeakReference 类来实现,它比软引用的生存周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内容空间是否足够,都会回收该对象占用的内存。
public static void main(String[] args) {
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1);
System.out.println(o1);
System.out.println(weakReference.get());
o1 = null;
System.gc();
System.out.println(o1);
System.out.println(weakReference.get());
}
4.虚引用
虚引用需要使用 java.lang.PhantomReference 类来实现。
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。PhantomReference 的 get方法总是返回 null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入 finalization 阶段,可以被 gc 回收,用来实现比 finalization 机制更灵活的回收操作。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
Java技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>
(o1,referenceQueue);
System.out.println(o1); // java.lang.Object@1b6d3586
System.out.println(phantomReference.get()); // null
System.out.println(referenceQueue.poll()); // null
System.out.println("===================");
o1 = null;
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println(o1); // null
System.out.println(phantomReference.get()); // null
System.out.println(referenceQueue.poll()); // java.lang.Object@1b6d3586
}
三、被标记的对象一定会死亡么?
事实上,被可达性分析标记为不可达的对象,依然有机会逃脱被回收的命运。因为一个对象被回收,至少需要经历两次标记过程。
第一次标记即为可达性分析算法的标记,在标记完成后,JVM将会对其进行一次筛选,筛选的条件是对象是否有必要执行finalize()方法。假如一个对象没有重写finalize()方法,或者finalize()方法已经被调用过,则虚拟机认为没有必要执行。
@Override
protected void finalize() throws Throwable {
super.finalize();
ReferenceGC.IsAlive = this;//在finalize()方法中重新与引用链对象建立连接
}
所有判定为需要执行finalize()方法的对象会被放置在一个名为F-Queue的队列中,并随后由一条由虚拟机自动创建的,低调度优先级的Finalizer线程去执行它们的finalize()方法。finalize()方法中若该对象成功与引用链上的对象建立连接,则在第二次标记时,将会被移除队列,从而存活。
但注意finalize()方法仅会被调用一次,无法调用第二次。