Java虚拟机笔记(二):GC垃圾回收和对象的引用

为什么要了解GC

我们都知道Java开发者在开发过程中是不需要关心对象的回收的,因为Java虚拟机的原因,它会自动回收那些失效的垃圾对象。那我们为什么还要去了解GC和内存分配呢?

答案很简单:当我们需要排查各种内存溢出、内存泄漏时,当垃圾收集器成为系统达到更高并发量的瓶颈时,我们就需要对这些"自动化"的技术实施必要的监控和调节。

 

回收哪些对象

我们知道在Java内存运行时数据区域中,虚拟机栈、本地方法栈和程序计数器是线程隔离的数据区,随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本是在类结构确定下来时就已知的。因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

而方法区和堆是由所有线程共享的数据区,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行时才知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。

 

如何判断对象可回收

1.引用计数法

原理:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

优点:实现简单,判断效率高。

缺点:很难解决对象之间相互循环引用的问题。(主流Java虚拟机里面没有选用该种方法)。

举个简单的例子,请看代码清单中的testGC()方法:对象objA和objB都有字段instance,赋值令objA.instance = objB及objB.instance = objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

 1 /**  
 2  * testGC()方法执行后,objA和objB会不会被GC呢?  
 3  * @author zzm  
 4  */  
 5 public class ReferenceCountingGC {  
 6  
 7   public Object instance = null;  
 8  
 9   private static final int _1MB = 1024 * 1024;  
10  
11   /**  
12    * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过  
13    */  
14   private byte[] bigSize = new byte[2 * _1MB];  
15  
16   public static void testGC() {  
17    ReferenceCountingGC objA = new ReferenceCountingGC();  
18    ReferenceCountingGC objB = new ReferenceCountingGC();  
19    objA.instance = objB;  
20    objB.instance = objA;  
21  
22    objA = null;  
23    objB = null;  
24  
25    //假设在这行发生GC,objA和objB是否能被回收?  
26    System.gc();  
27   }  
28 } 

运行结果

 1 [Full GC (System) [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]  
 2 Heap  
 3  def new generation   total 9216K, used 82K [0x00000000055e0000, 0x0000000005fe0000, 0x0000000005fe0000)  
 4   Eden space 8192K,   1% used [0x00000000055e0000, 0x00000000055f4850, 0x0000000005de0000)  
 5   from space 1024K,   0% used [0x0000000005de0000, 0x0000000005de0000, 0x0000000005ee0000)  
 6   to   space 1024K,   0% used [0x0000000005ee0000, 0x0000000005ee0000, 0x0000000005fe0000)  
 7  tenured generation   total 10240K, used 210K [0x0000000005fe0000, 0x00000000069e0000, 0x00000000069e0000)  
 8    the space 10240K,   2% used [0x0000000005fe0000, 0x0000000006014a18, 0x0000000006014c00, 0x00000000069e0000)  
 9  compacting perm gen  total 21248K, used 3016K [0x00000000069e0000, 0x0000000007ea0000, 0x000000000bde0000)  
10    the space 21248K,  14% used [0x00000000069e0000, 0x0000000006cd2398, 0x0000000006cd2400, 0x0000000007ea0000)  
11 No shared spaces configured. 

从运行结果中可以清楚看到,GC日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

 

2.可达性算法(根搜索算法)

原理:通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

 

图中,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

 

对象的引用 

无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。  

1.强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:

 Object o = new Object(); // 强引用 

当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:

 o = null; // 帮助垃圾收集器回收此对象 

显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于gc的算法。

 

2.软引用(SoftReference)

用来描述一些还有用,但并非必需的对象。

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

1 String str = new String("abc");                                     // 强引用
2 SoftReference<String> softRef = new SoftReference<String>(str);     // 软引用

当内存不足时,等价于:

1 If(JVM.内存不足()) {
2    str = null;  // 转换为软引用
3    System.gc(); // 垃圾回收器进行回收
4 }

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建

(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出

这时候就可以使用软引用。

1 Browser prev = new Browser();               // 获取页面进行浏览
2 SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用        
3 if(sr.get()!=null){ 
4     rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
5 }else{
6     prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了
7     sr = new SoftReference(prev);       // 重新构建
8 }

这样就很好的解决了实际的问题。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

 

3.弱引用(WeakReference)

也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

弱引用与软引用的区别:

只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

String str=new String("abc");
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
str=null;

当垃圾回收器进行扫描回收时等价于:

str = null;
System.gc();

如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。   

下面的代码会让str再次变为一个强引用:

String  abc = abcWeakRef.get();

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。

 

4.虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。

在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

虚引用,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

我们可以声明虚引用来引用我们感兴趣的对象,在GC要回收的时候,GC收集器会把这个对象添加到ReferenceQueue,这样我们如果检测到ReferenceQueue中有我们感兴趣的对象的时候,说明GC将要回收这个对象了。此时我们可以在GC回收之前做一些其他事情,比如记录些日志什么的。

 

4种引用的总结

Java4种引用的级别由高到低依次为:

强引用  >  软引用  >  弱引用  >  虚引用

通过表格来看一下他们之间在垃圾回收时的区别:

 

对象死亡的两次标记过程 

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

1).第一次标记并进行一次筛选。

筛选的条件是此对象是否有必要执行finalize()方法。

当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

2).第二次标记

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。

finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
流程图如下: 

一次对象自我拯救的演示

 1 /**
 2  * 此代码演示了两点
 3  * 1、对象可以在被GC时自我拯救
 4  * 2、这种自救的机会只有一次,因为一个对象的finalize()方法最多只能被系统自动调用一次。
 5  */
 6 public class FinalizeEscapeGC {
 7     public static FinalizeEscapeGC SAVE_HOOK = null;
 8 
 9     public void isAlive() {
10         System.out.println("yes, I am still alive");
11     }
12 
13     protected void finalize() throws Throwable {
14         super.finalize();
15         System.out.println("finalize method executed!");
16         FinalizeEscapeGC.SAVE_HOOK = this;
17     }
18 
19     public static void main(String[] args) throws InterruptedException {
20         SAVE_HOOK = new FinalizeEscapeGC();
21 
22         //对象第一次成功拯救自己
23         SAVE_HOOK = null;
24         System.gc();
25 
26         //因为finalize方法优先级很低,所有暂停0.5秒以等待它
27         Thread.sleep(500);
28         if (SAVE_HOOK != null) {
29             SAVE_HOOK.isAlive();
30         } else {
31             System.out.println("no ,I am dead QAQ!");
32         }
33 
34         //-----------------------
35         //以上代码与上面的完全相同,但这次自救却失败了!!!
36         SAVE_HOOK = null;
37         System.gc();
38 
39         //因为finalize方法优先级很低,所有暂停0.5秒以等待它
40         Thread.sleep(500);
41         if (SAVE_HOOK != null) {
42             SAVE_HOOK.isAlive();
43         } else {
44             System.out.println("no ,I am dead QAQ!");
45         }
46     }
47 }

运行结果

1 finalize method executed!
2 yew, I am still alive 
3 no ,I am dead QAQ!

从结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。

注意:任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了,并且建议大家尽量避免使用它 。

同类随笔

Java虚拟机笔记(一):类加载机制

Java虚拟机笔记(二):GC垃圾回收

Java虚拟机笔记(三):垃圾收集算法

Java虚拟机笔记(四):垃圾收集器

Java虚拟机笔记(五):JVM中对象的分代

 

参考资料:

1.《深入理解Java虚拟机:JVM高级特性与最佳实践》 

2. https://my.oschina.net/ydsakyclguozi/blog/404389

3. http://blog.csdn.net/ochangwen/article/details/51406779

 

posted @ 2017-06-17 09:30  明志健致远  阅读(1694)  评论(0编辑  收藏  举报