垃圾回收机制(GC) Java内存区域及对象

前言

  上一篇文章Java内存区域及对象讲述了Java内存运行时的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程生而生,随线程灭而灭,在这几个区域是不需要过多的考虑回收的问题的,因为方法结束或者线程结束时,内存自然就跟随着回收了;而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分的内存。

哪些对象需要回收

  在堆中存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是要确定这些对象之中哪些还“活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)了。

1.引用计数法

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

  客观来说,引用计数算法实现很简单,判定效率也很高,在大部分的情况下它都是一个不错的算法,但是也有一个缺点就是:它很难解决对象之间相互循环引用的问题。

 1 /**
 2  * 虚拟机参数:-verbose:gc
 3  */
 4 public class ReferenceCountingGC
 5 {
 6     private Object instance = null;
 7     private static final int _1MB = 1024 * 1024;
 8 
 9     /** 这个成员属性唯一的作用就是占用一点内存 */
10     private byte[] bigSize = new byte[2 * _1MB];
11 
12     public static void main(String[] args)
13     {
14         ReferenceCountingGC objectA = new ReferenceCountingGC();
15         ReferenceCountingGC objectB = new ReferenceCountingGC();
16         objectA.instance = objectB;
17         objectB.instance = objectA;
18         objectA = null;
19         objectB = null;
20 
21         System.gc();
22     }
23 }

输出结果:

[0.109s][info][gc] Using G1
[0.221s][info][gc] GC(0) Pause Full (System.gc()) 8M->1M(10M) 3.719ms

上述代码中objectB和objectA,只是在16、17行中相互引用,除此之外,再也没有其他地方使用过。

从输出结果看,虽然两个对象相互引用着,但是虚拟机还是把这两个对象回收了,这说明了虚拟机并不是使用的引用计数法来判定对象是否还活着的。

2.可达性分析

  可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图所示:

图片中,object5、object6、object7虽然相互有关联,但是他们到GC Roots是不可达的,所以他们将会判定为可回收对象。

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

  ❤ 虚拟机栈(栈帧中的本地变量表)中引用的对象;

  ❤ 方法区中类静态属性引用的对象;

  ❤ 方法区中常量引用的对象;

  ❤ 本地方法栈中JNI(即一般说的Native方法)引用的对象。

在了解垃圾回收机制之前,我们需要了解一些关于引用以及垃圾回收的一些常识:

引用:

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

  ❤ 强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object();”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

  ❤ 软引用用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常,使用SoftReference类来实现软引用。

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

  ❤ 虚引用也称为幽灵引用或者幻影引用,它时最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾回收器回收时收到一个系统通知。使用PhantomReference类来实现虚引用。

生存还是死亡:

  即使在可达性算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于缓刑阶段,要宣告一个对象死亡,至少需要经历两次的标记过程:如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。

  如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它,这里所说的“执行”是指虚拟机会触发这个方法,但并不会承诺等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功解救自己-----只要重新与引用链上的任何一个对象建立关联即可,比如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将会被移除“即将回收”的集合,如果对象这时候还没有逃脱,那基本上就真的被回收了。

方法区的回收

  很多人认为方法区(HotSpot虚拟机中的永久代)是没有垃圾回收的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾回收,而且在方法区中进行垃圾回收的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾回收一般可以收集70%~95%的空间,而永久代的垃圾回收效率远低于此。

  永久代垃圾收集主要回收两部分内容:废弃常量和无用的类。

  废弃常量:没有任何对象引用常量池中的某个常量,同时也没有其他地方引用了这个字面量,那么这个常量就是废弃常量。

  无用的类需要满足以下三个条件:

    (1)该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例;

    (2)加载该类的ClassLoader已经被回收;

    (3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;

垃圾回收算法

1.标记 - 清除(Mark-Sweep)算法

  这是最基础的算法,标记 - 清除算法就如同它的名字样,算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记 - 清除算法的缺点:

  ❤ 标记和清除两个过程的效率都不高;

  ❤ 标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次的垃圾收集动作;

标记 - 清除算法的执行过程如下图所示:

2.复制(Copying)算法

  复制算法是为了解决效率问题而出现的,它将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动指针,按顺序分配内存即可。

  复制算法的执行过程如下所示:

复制算法的优点是:实现简单,运行高效;当然也有个缺点:内存缩小为原来的一半,代价比较高。

  现在的商业虚拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Suivivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例是8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保。

3.标记 - 整理(Mark - Compact)算法

  复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要有额外的空间进行分配担保。老年代都是不易回收的对象,对象的存活率高,因此一般不能直接选用复制算法。根据老年代的特点,提出了标记 - 整理算法,过程和标记 - 清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。标记 - 整理算法的执行过程如下图所示:

这种算法的优点是:既没有内存空间碎片,也没有浪费50%的内存空间。

4.分代收集(Generational Collection)算法

  用一张图概括堆内存的布局:

  现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没有什么特别的,无非就是上面几种算法的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适合的收集算法。大批的对象死去、少量存活的,使用复制算法;对象存活率高,没有额外空间进行担保的,采用标记-清理或者标记-整理算法。

GC的触发时机

最后总结一下什么时候会触发一次GC,个人经验看,有三种场景会触发GC:

  1、第一种场景应该很明显,当年轻代或者老年代满了,Java虚拟机无法再为新的对象分配内存空间了,那么Java虚拟机就会触发一次GC去回收掉那些已经不会再被使用到的对象

  2、手动调用System.gc()方法,通常这样会触发一次的Full GC以及至少一次的Minor GC

  3、程序运行的时候有一条低优先级的GC线程,它是一条守护线程,当这条线程处于运行状态的时候,自然就触发了一次GC了。

 参考:《深入理解Java虚拟机》 周志明 编著:

猜你喜欢

转载自www.cnblogs.com/Joe-Go/p/9911765.html