四、GC 垃圾回收(二)
4.3 垃圾判定
- 如何认为堆上的对象是垃圾呢?
Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还 在使用,不允许被回收。
public class Demo {public static void main(String[]args){Demo demo = new Demo();demo = null;}
}
引用计数法
引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:
- 简单,高效,现在的objective-c、python等用的就是这种算法。
缺点:
- 引用和去引用伴随着加减算法,影响性能
- 很难处理循环引用的两个对象则无法释放。
因此目前主流的 Java 虚拟机都摒弃掉了这种算法,默认的是可达性分析算法。
要说明这个图中的引用计数法的过程:
所以这里obj -> a/b
之后,也就是间接引用了对象。
虽然他们最后存在循环引用了,但是这两个对象实际上已经不可达了(没有任何外部引用能访问到它们)。
A a = new A();
B b = new B();
a.obj = b; // B对象引用计数: 2 (变量b + A对象的obj字段)
b.obj = a; // A对象引用计数: 2 (变量a + B对象的obj字段)
a = null; // A对象引用计数: 1 (只剩B对象的obj字段引用)
b = null; // B对象引用计数: 1 (只剩A对象的obj字段引用)
这里的finalize()
方法在后面就能马上看到是为什么了。
public class ReferenceCount {private static Object instance;/*** 前提是重写* 任意一个对象在回收的时候会调用该对象的finalize()方法* @throws Throwable*/@Overrideprotected void finalize() throws Throwable {System.out.println("对象被回收了");}public static void main(String[] args) {// 定义一个a对象ReferenceCount a = new ReferenceCount();// 定义一个b对象ReferenceCount b = new ReferenceCount();//b对象被a对象引用a.instance=b;//a对象被b对象引用b.instance=a;// a b对象要被GC垃圾回收器回收掉a=null;b=null;try {Thread.sleep(200);//让System.gc()能够被执行到。} catch (InterruptedException e) {throw new RuntimeException(e);}System.gc();// 让jvm进行一次fullgc.// 因为现在java中没有用引用计数法,所以即使出现了对象的相互引用,依然可以让这些对象被回收掉。}
}
可达性分析
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
这里能看出来可不可达,得是被里面往外走,外面往里没人用你!
- 所以其实最重要的问题是区分哪些是
GC Root
?
在Java语言中,可以作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象。
- 方法区中的类静态属性引用的对象。
- 方法区中的常量引用的对象。
case1:
public void a() {Object obj = new Object();// obj可以作为gc_roots;
}case2:
private static User user = new User();//user可以作为gc_roots;case3:
private static fina Map map = new HashMap(10);//order可以作为gc_roots; 11 12 13 都可以 final 修改引用类型private static fina int a=1;
这里一个重点知识:一旦某个常量是引用类型 那么代表的是这个常量对应的对象地址值不可以变,而这个对象的属性可以任意修改,所以可以new HashMap(11 12 13);
- 本地方法栈中JNI(Native方法):这种很少了解一下,通过 Java Native Interface (JNI) 在 Java 代码和本地代码之间传递的对象也被视为 GC Root 对象。这些对象的生命周期由本地代码管理。
理解小技巧:
由于Root 采用栈方式存放指针,所以如果一个指针,它保存了堆里面的对象,但是自己又不存放在堆里面,那他就可以作为一个Root。上面的几个例子就是这样的。
//a变量的值未来成为一个常量。这个值是不可变的。
private static final int a = 1;//b指向的这个String对象地址值是不可变,而不是String未来里面只能存放一个字符串"b"
private static final String b = new String("b");
在1.7及之后,将方法区中的字符串常量池和静态变量
放到了堆中去了。
1.8 及以后,将方法区的实现从永久代 -> 元空间
,并且从 JVM 运行时区域挪出去了。
- 如果要使用可达性算法来判断是否可以回收
那么分析工作必须要在一个能保证一致性快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致GC时进行必须“Stop the world”的一个重要原因。
即使号称不会发生停顿的CMS收集器中,枚举节点是也是要必须停顿的。
Finalization 机制
那些没有直接或间接可达的对象,不能断定就说是就是垃圾,或者说立马就能够被 GC 回收。在真正成为垃圾之前,会经历的一个标记过程,就叫做finalization
机制。
太过官方的说法:
- 没有被引用的对象被 GC 之前,总会先调用这个对象的
finalize()
方法 finalize()
方法允许被子类重写,并在对象回收时进行释放资源。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件,套接字和数据连接等。- 永远不要主动调用某个对象的finalize()方法,应交给垃圾回收器调用,理由如下:
- 在finalize()时可能会导致对象的复活
- finalize()方法执行时没有保障的,完全有GC线程决定,在极端情况下,若不发生GC,则finalize()方法没有执行机会
- 由于finalize()方法的存在,虚拟机中对象一般处于三种可能的状态:
- 可触及的,从根节点开始,可以到达这个对象
- 可复活的,对象的所有引用都被释放,但是对象有可能在finalize()中复活
- 不可触及的,对象的finalize()被调用,并且没有复活,那么就进入不可触及的状态。不可触及的对象不能被复活的,因为finalize()只会被调用一次。
因此,由于finalize()的存在,进行区分,只有对象在不可触及时才可以被回收。
如果从所有的根节点都无法访问某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收,但事实上,这个对象也并非是“非死不可的”,这时候它们处于“缓刑”阶段。
重点:
- 没有被引用的对象被 GC 之前,总会先调用这个对象的
finalize()
方法(如果重写了可能就会复活) - 任一一个对象的
finalize
方法,只会被System.gc()
现成调用一次
就是这个机制,可以复活!
没有引用链还想复活,就需要这个机制。
怎么挂到 GC root 链条上呢?能复活呢?
【复活的具体过程】
判断一个对象objA是否可回收,至少经历两次标记过程:
- 如果对象objA到GC Roots没有引用链,则进行第一次标记。
- 进行筛选,判断此对象是否有必要执行finalize()方法
- 如果对象objA
没有重写
finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA判定为不可触及。 - 如果对象objA
重写了
finalize()方法,并且尚未执行过,那么objA会被插入到F-QUEUE队列中,由一个虚拟机创建的,低优先级的finalizer线程去执行finalize()方法。 - finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-QUEUE中的对象进行第二次标记。如果objA对象在finalize()方法中与引用链上的任意一个对象建立了联系,那么在第二次标记是,objA对象会被移除“即将回收”集合。之后,对象会在次出现没有引用的存在情况,在这个情况下,finalize()方法是不会被再次调用的,对象直接变为不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次。
- 如果对象objA
首先是正常情况下,对象引用还在,finalize
方法重写是不起作用...
这里不再展示各种情况,就是本身有对象,然后 null
再去改写 finalize -> this,发现复活
然后第二次 null,发现死了,而且这次没有触发 finalize 方法
如果是改写 finalize -> new 对象,发现复活了,而且触发了 finalize 方法
- 为什么之前复制是 this 呢?
- 为 重新赋值这个类型的对象,这两个意思是一样的,效果一样吗?
@Overrideprotected void finalize() throws Throwable {System.out.println("HeapDemo6 object inner finalize ivoke");// 让heapDemo6重新和gc_roots做一个关联。
// heapDemo6 = this;// HeapDemo6对象复活heapDemo6 = new HeapDemo6();// HeapDemo6对象复活 // 0x12 0x13}
- 这是为什么呢?
任一一个对象试一次,如果是 new Heap();
如果是 this,对象就是那一个;如果是重新 new 了,类型是这个类型,但那就是另外一个对象的地址了,finalize 可以重新触发,否则只能触发一次。
完整的代码
public class HeapDemo6 {private static HeapDemo6 heapDemo6 = null;// 任意一个对象的finalize方法只会被gc线程调用一次。@Overrideprotected void finalize() throws Throwable {System.out.println("HeapDemo6 object inner finalize ivoke");// 让heapDemo6重新和gc_roots做一个关联。
// heapDemo6 = this;// HeapDemo6对象复活heapDemo6 = new HeapDemo6();// HeapDemo6对象复活 // 0x12 0x13}public static void main(String[] args) throws Exception {heapDemo6 = new HeapDemo6(); // 0x11heapDemo6 = null;// 未来可以让改对象复活 第一次你该对象从引用链中断掉System.gc();Thread.sleep(200);if (heapDemo6 == null) {System.out.println("heapDemo6 is dead");} else {System.out.println("heapDemo6 is alive");}System.out.println("------------------------");heapDemo6 = null;// 未来可以让改对象复活 第二次又让该对象从引用链中断掉 0x12=null;System.gc();Thread.sleep(200);if (heapDemo6 == null) {System.out.println("heapDemo6 is dead");} else {System.out.println("heapDemo6 is alive");}}
}
强软弱虚
引用类型 | 特点 | 实现 | 生命周期 |
强 | Object obj = new Object() | 引用在就不会被回收 |
软 | SoftReference | 活在 OOM 之前 |
弱 | WeakReference | 活在下一次 GC 之前 |
虚 | PhantomReference | 随时被回收 |
强引用:
反射和new出来的对象都是强引用类型。
软引用:
爆 OOM 前的 GC 会将软引用回收掉,如果空间还是不够,才会 OOM。
软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存;当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
虚引用:
一个持有虚引用的对象和没有引用几乎是一样的,随时可能被垃圾回收器回收。
/*** Description: 测试对象的类型(强 软 弱 虚)* 强:生命周期:即使oom,也会保留改类型的对象* 软:生命周期:oom之前(没有oom gc之前 该对象在 gc之后 该对象也在)* 弱:生命周期:一次gc之前(gc之前该对象有 gc之后 该对象就不在)* 虚:生命周期:刚创建出来就没有了。(gc之前没有 gc之后也没有)*/
public class HeapDemo7 {public static void main(String[] args) {// softReference();// 软引用// weakReferenceTest();// 弱引用phantomReference();// 虚引用}/*** 软引用测试* 会发现gc后,软引用对象的值获仍然能够获取到*/private static void softReference() {String str = new String("测试值");SoftReference<String> stringSoftReference = new SoftReference<>(str);System.out.println("软引用的值" + stringSoftReference.get());//没有进行gc前软引用能得到对象str = null; // str被回收System.gc();stringSoftReference.get();System.out.println("软引用对象被垃圾回收了,软引用对象的值" + stringSoftReference.get());}/*** 弱引用测试* 会发现gc后,弱引用对象的值获取不到*/private static void weakReferenceTest() {String str = new String("测试值");WeakReference<String> stringWeakReference = new WeakReference<>(str);str = null;System.out.println("软引用的值" + stringWeakReference.get());//没有进行gc前软引用能得到对象System.gc();//进行垃圾回收stringWeakReference.get();System.out.println("软引用对象被垃圾回收了,软引用对象的值" + stringWeakReference.get());}/*** 虚引用测试* 会发现gc前,弱引用对象的值都获取不到*/private static void phantomReference() {String helloWorldString = new String("测试值");ReferenceQueue queue = new ReferenceQueue();PhantomReference ref = new PhantomReference(helloWorldString, queue);System.out.println(ref.get());System.gc();//进行垃圾回收System.out.println(ref.get());}
}