一,介绍
什么是垃圾? 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中的垃圾进行清理,这些 垃圾会一直保留到程序结束,被保留的空间无法被其它对象使用,甚至会导致内存溢出。
2,为何需要GC 回收内存空间再分配 可以清理内存中的记录碎片,碎片整理将所占有的堆内存移动堆的另一端。
二,垃圾标记阶段
在GC执行垃圾回收之前,首先需要区分内存中那些事存活的对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收事释放掉其所占用的内存空间。
如何判断对象已经死亡? 当一个对象不在被任何存活对象继续引用时,就可以宣判为已经死亡。判断对象存活一般有两种方式: 引用计数算法 可达性分析算法
2.1,引用计数算法
对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效是,引用计数器就减1.只要对象A的引用计数器的值为0, 则表示对象A不可能再被使用,可以进行回收。
2.1.1,优点
实现简单,垃圾对象便于辨识;
判定效率高,回收没有延迟性。
2.1.2,缺点:
它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
每次赋值都需要更新计数器,伴随着加法和减法操作,正价了时间开销。
无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
2.2,可达性分析算法(根搜索算法,追踪性垃圾收集)
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中 循环引用的问题,防止内存泄漏的发生。
2.2.1,基本思路
可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上到下的方式所有被根对象集合所联机的目标对象是否可达。 使用可达性分析算法后,内存中的存活对象集合直接或间接连接中,搜索所走过的路径称为引用链(Reference Chain)。 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。 在可达性分析算法中,只有被根对象集合直接或者间接连接的对象才是存活对象。
2.2.2,GC Roots(根对象集合)
1,虚拟机栈中引用的对象 比如:各个线程中被调用的方法中使用到的参数,局部变量等。 2, 本地方法栈栈内JNI(本地方法)引用的对象 3, 方法区中类静态属性引用的对象 比如:Java类的引用类型静态变量。 4,方法区中常量引用的对象 比如:字符串常量池(String Table)里的引用、 5, 所有被同步锁synchronized持有的对象 6,Java虚拟机内部的引用 基本数据类型对应的Class对象,一些常驻的异常对象(NullPointerException,OutofMemoryError),系统类加载器。 7,反应Java虚拟机内部情况的JMXbean,JVMTI中注册的回调,本地代码缓存等。
小技巧: 由于Root采用栈方式存放变量和指针,所以如果一个指针,她保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
三,垃圾清除阶段
当成功区分去内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占有的内存空间,以便有足够的可用内存空间为新对象分配内存。 常见的垃圾清除算法: 标记-清除算法(Mark-Sweep) 复制算法(Copying) 标记-压缩算法(Mark-Compact)
3.1, 标记-清除算法(Mark-Sweep)
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也称之为stop the world),然后进行两项工作,标记和清除。
3.1.1,标记
Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
3.1.2,清除
Collector对堆中内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
3.1.3,优缺点
优点: 实现简单 缺点: 效率不高,需要遍历全部可达对象。 在进行GC的时候需要停止整个程序。 这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲空间列表。 这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否足够,够的话就存放(覆盖)。
3.2,复制(Copying)算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中存活的对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后回收垃圾。(和新生代中surviver1,surviver2一样)
3.2.1,优缺点
优点: 没有标记和清理的过程,实现简单,运行高效。 复制过去以后保证空间的连续性,不会出现“碎片”问题。 ** 缺点**: 有一半的可用内存不可用。 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用,不管是内存占用或者时间开销也不小。 可达对象多,垃圾少的情况下,该算法并不高效,需要移动的对象太多,比如老年代,方法区中。适合在新生代中使用。
3.3,标记-压缩(标记整理,Mark-Compact)算法
复制算法的高效性是建立在存活对象少,垃圾多的前提下,这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。因此,基于老年代垃圾回收的特性,需要使用其他算法。
3.3.1,标记
第一阶段和标记清除算法一样,从根节点开始标记所有被可达对象。
3.3.2,压缩
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放,之后清理边界外所有空间。
3.3.3.,标记压缩算法和标记清除算法的区别
标记压缩算法的最终效果等同于标记清除算法执行完后,再进行一次内存碎片整理,因此,也可以把它称之为标记-清除-压缩算法。 二者的本质区别在于标记-清除是一种非移动式的垃圾回收算法,标记-压缩是移动式的,是否移动回收后的存活对象是一项优缺点并存的风险决策。
3.3.4,优缺点
优点: 不会导致内存碎片,不需要维护空闲空间的地址列表,新分配内存时只需要持有一个内存的起始位置即可。 消除了复制算法中,可用内存减半的高额代价。 缺点: 效率上,标记-压缩算法要低于复制算法。 移动对象的同时(地址变了),还需要调整其他引用对象的引用地址。 移动过程中,需要全程暂停用户应用程序。
效率上,复制算法时最快的,但是会浪费内存空间。标记压缩算法比较中庸,比复制算法多了个标记阶段,比标记清除算法多了个整理内存的阶段。
3.4,分代收集算法
几乎所有的GC都是采用分代收集算法执行垃圾回收。 年轻代: 区域相对老年代小,对象生命周期短,存活率低,回收频繁。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收,而复制算法内存利用率不高的问题,通过survivor的设计得到缓解(一个survivor默认只占用了1/3*1/10=1/30的堆内存)。 老年代: 区域较大,对象生命周期长,存活率高,回收不及年轻代频繁。这种情况存在大量存活率高的对象,复制算法变得不合适。一般使用标记清除算法或者和标记压缩算法的混用。
3.5,增量收集算法
上述的三种基本收集算法在垃圾回收过程中都会导致应用程序的所有线程挂起,暂停一切正常的工作,如果GC时间过长,将严重影响用户体验或者系统稳定性。为了解决这个问题,诞生了增量收集算法(Increment Collecting)。 基本思想: 如果一次性将所有的垃圾进行处理 ,需要造成系统较长时间的停顿,那么就可以让那个垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应有程序线程,依次反复,直到垃圾收集完成。 总的来说,增量收集算法的基础仍是基本的标记清除和复制算法。增量收集算法通过对线程冲突的妥善处理,允许垃圾垃圾收集线程以分阶段的方式完成标记清理复制的工作。
优点 : 缩短每次GC导致的应用程序停顿的时间。
缺点: 线程切换和上下文转换的消耗,会使的垃圾回收的总成本上升,造成系统吞吐量的下降。
3.6,分区收集算法
一般来说,在相同条件下堆空间越大,一次GC所需要的时间就越长,有关GC的停顿也越长。如果将一块大的内存区域风格成多个小块,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。 分代算法按照对象生命周期长短分为两部分(新生代,老年代),而分区算法将整个堆空间划分为连续的不同小区间。每一个小区间独立使用独立回收。这种算法的好处是可以控制一次回收多少个小区间。
四,对象的finalization机制
Java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,垃圾回收器总会调用这个对象的**finalize()**方法。 **finalize()**方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库连接等。
永远不要主动调用某个对象的**finalize()**方法,应该交给垃圾回收机制调用。 理由: **finalize()**可能导致对象复活; **finalize()**方法的执行时间是没有保障的,她完全由GC线程决定,极端情况下若不发生GC,**finalize()**方法没有执行机会。 一个糟糕的finalize()会严重影响GC性能。
4.1,对象复活
如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说此对象需要被回收。但事实上,也并非非死不可,这时候他们暂时处于缓刑阶段,一个无法触及的对象有可能在某一个条件下复活自己,如果这样,那么对它的回收就是不合理的。
4.1.1,对象的状态
可触及的:从根节点开始,可以到达这个对象。 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及状态不可能被复活,因为finalize()只会被调用一次。
4.1.2,复活过程
判定一个对象是否需要被回收,至少需要经历两次标记过程: 1,GC Roots没有到该对象的依赖链,则进行第一次标记。 2,进行筛选,判断此对象是否有必要执行finalize()方法。 如果对象没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过没准儿虚拟机视为“没有必要执行”,对象被判定为不可触及。 如果重写了finalize()方法,且还未执行过,那么该对象会被插入到F-Queue队列中,由一个虚拟机自动创建低优先级的Finalizer线程执行其finalize()方法; finalize()方法是逃避死亡的最后机会,如果在该方法中对象与引用链上的任何一个对象建立了联系,那么在第二次标记时,该对象会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况,这时候finalize()方法不会被再次调用,对象直接变成不可触及的状态,finalize()只会被调用一次。
package jvm;
public class Node {
public static Node primaryNode; //类静态变量 GC Root
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize 执行");
primaryNode=this;
}
public static void main(String[] args) throws Throwable {
primaryNode=new Node();
primaryNode=null;
System.gc();
System.out.println("第一次 gc");
Thread.sleep(3000);
System.out.println("node is null? "+(primaryNode==null));
primaryNode=null;
System.gc();
System.out.println("第二次 gc");
Thread.sleep(3000);
System.out.println("node is null? "+(primaryNode==null));
}
}
|