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)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

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()方法不会被再次执行,因此第二段代码的自救行动失败了,并且建议大家尽量避免使用它 。

参考资料:

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

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

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

时间: 2025-01-15 01:35:03

Java虚拟机笔记(二):GC垃圾回收的相关文章

深入理解java虚拟机笔记(二)-垃圾回收

ps:文中的图片都来自网络.部分图片来源 1. 前言 作为一种高级语言,比起c和c++来,很进步的一点就是垃圾回收机制.这省去来了我们很多的工作,不过,我们仍然需要了解垃圾回收,这对我们的成长很有帮助. 2. 引用计数法 引用计数法在很多高级语言都有,如python,java也不例外.对象内部维护有一个被其他对象引用的引用计数,当这个引用计数为0的时候,表示对象可以被回收. 引用计数法存在一个问题,就是循环引用,加入a引用b,b同时也引用a,那么就存在ab的引用计数都不为0的情况. 3. 可达性

Java虚拟机内存模型及垃圾回收监控调优

Java虚拟机内存模型及垃圾回收监控调优 如果你想理解Java垃圾回收如果工作,那么理解JVM的内存模型就显的非常重要.今天我们就来看看JVM内存的各不同部分及如果监控和实现垃圾回收调优. JVM内存模型         正如你上图所看到的,JVM内存可以划分为不同的部分,广义上,JVM堆内存可以划分为两部分:年轻代和老年代(Young Generation and Old Generation) 年轻代(Young Generation) 年轻代用于存放由new所生成的对象.当年轻代空间满时,

探秘Java虚拟机——内存管理与垃圾回收

详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt106 1.Java虚拟机运行时的数据区 2.常用的内存区域调节参数 -Xms:初始堆大小,默认为物理内存的1/64(<1GB):默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制 -Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小

探秘Java虚拟机——内存管理与垃圾回收(转)

本文主要是基于Sun JDK 1.6 Garbage Collector(作者:毕玄)的整理与总结,原文请读者在网上搜索. 1.Java虚拟机运行时的数据区 2.常用的内存区域调节参数 -Xms:初始堆大小,默认为物理内存的1/64(<1GB):默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制 -Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

深入理解Java虚拟机笔记---内存分配与回收策略

Java技术体系中的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存.对象的内存分配往大的方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲(-XX:+UseTLAB,默认已开启),将按线程优先在TLAB上分配.少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置. 下面是几条主要的最普遍的内存分配规则: 1.对象优先在Ed

java虚拟机——内存管理与垃圾回收机制

一  java内存区域与内存溢出异常(OOM) 1.运行时数据区域划分  

Java中内存泄露及垃圾回收机制

3 垃圾回收机制 3.1 什么是垃圾 垃圾,内存中的垃圾,即内存中已无效但又无法自动释放的空间.在Java语言中,没有引用句柄指向的类对象最容易成为垃圾.,产生垃圾的情况有很多,主要有以下3种: (1)       超出对象的引用句柄的作用域时,这个引用句柄引用的对象就变成垃圾. 例: { Person p1 = new Person(); …… } 引用句柄p1的作用域是从定义到“}”处,执行完这对大括号中的所有代码后,产生的Person对象就会变成垃圾,因为引用这个对象的句柄p1已超过其作用

(转)Java 内存区域分配和垃圾回收(GC)机制

Java垃圾回收概况 Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代 码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢.这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制.概括地说,该机制对 JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop

面试官,不要再问我“Java GC垃圾回收机制”了

Java GC垃圾回收几乎是面试必问的JVM问题之一,本篇文章带领大家了解Java GC的底层原理,图文并茂,突破学习及面试瓶颈. 楔子-JVM内存结构补充 在上篇<JVM之内存结构详解>中有些内容我们没有讲,本篇结合垃圾回收机制来一起学习.还记得JVM中堆的结构图吗? 图中展示了堆中三个区域:Eden.From Survivor.To Survivor.从图中可以也可以看到它们的大小比例,准确来说是:8:1:1.为什么要这样设计呢,本篇文章后续会给出解答,还是根据垃圾回收的具体情况来设计的.

深入理解java虚拟机(二)HotSpot Java对象创建,内存布局以及访问方式

内存中对象的创建.对象的结构以及访问方式. 一.对象的创建 在语言层面上,对象的创建只不过是一个new关键字而已,那么在虚拟机中又是一个怎样的过程呢? (一)判断类是否加载.虚拟机遇到一条new指令的时候,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号代表的类是否被加载.解析并初始化.如果没有完成这个过程,则必须执行相应类的加载. (二)在堆上为对象分配空间.对象需要的空间大小在类加载完成后便能确定.之后便是在堆上为该对象分配固定大小的空间.分配的方式也有两种: