写在前面:GC的主要对象是虚拟机运行时数据区域堆内存中的对象实例,从内存回收角度看,因为如今的收集器基本都采用分代收集(Generational Collection)算法,所以java堆中内存还能够进一步细分:新生代和老年代。针对新生代与老年代的各自特征,为更好地提升收集效率及产生更少(甚至不产生)内存碎片,收集器使用的收集算法也并不相同。html
宏观角度讲,GC作的事情能够从三方面看:java
(1)what :收集哪些对象实例?算法
(2)how:怎样回收?函数
(3)when:什么时候回收?线程
收集哪些对象实例?指针
查找哪些对象待回收的算法通常有两种:a、引用计数法 b、可达性算法htm
a、引用计数法:对象
思想:当一个对象被另一个对象引用时,则该对象的引用计数器值加1,当引用链失效时计数器值减1,当GC被触发时,若是对象引用计数器值为0则表示该对象能够回收。blog
实现:实现方式主要有两种:一种是侵入式,一种是非侵入式 ;侵入式表示计数器所需内存在该对象内存中分配,或者说计数器变量被定义在该对象中,在对象的构造函数和复制函数中对计数器加1,在对象的析构函数中减1(C语言为例)。非侵入式的不一样地方是引用计数器在单独的内存中管理。队列
优势:实现简单
不足:很难解决对象间循环引用的问题
b、可达性算法:
思想:经过一系列称为"GC Roots"的对象做为起始点,从这些节点开始向下搜索,所走过的路经称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的。
在Java语言中,可做为GC Roots的对象包括下面几种:
不难发现上面的几种对象都是对象引用间的顶级对象。
在主流的商用程序语言的主流实现中,都是使用可达性分析来断定对象是否存活的。当一个对象进过可达性分析后发现其与"GC Roots"不可达,此时便会对该对象进行第一次标记,再次通过筛选后才能最终断定该对象是否须要回收,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()或者finalize()方法已经被虚拟机调用过期该对象将被最终肯定为待回收对象(虚拟机将这两种状况视为"没有必要执行");
若是这个对象被断定为有必要执行 finalize()方法。那么这个对象将会被放置在一个叫作F-Queue队列中,并在稍后由一个虚拟机自动创建的、低优先级的Finalizer线程去执行它。此线程只会去触发方法的运行,并不保证方法运行结束(防止队列中的其余待执行对象长时间等待,致使整个内存回收系统崩溃),当一个对象在本身的 finalize()方法中与引用链上的对象创建了关联,则本次不会回收该对象,反之则会被标记为待回收对象。
怎样回收?
回收算法:
a、标记-清除算法
思路:该算法分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象,它的标记过程即用上述两种标记算法实现。
不足:(1)效率问题,标记和清除两个过程效率都不高;
(2)空间问题,因为是直接清除待回收对象,则会致使大量内存碎片,空间碎片太多可能进而致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。
b、复制算法
为了解决效率问题,复制(coping)算法油然而生,概念模型上它将可用的内存分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区内存进行回收,内存分配时也就不用考虑内存碎片的问题,直接移动堆顶指针,按顺序分配便可,实现简单,运行高效。
不足点也很明显,内存代价太大。
目前商用虚拟机基本都采用这种收集算法来回收新生代。实际上在新生代中也不是按照1:1的比例来划份内存空间,而是讲内存分为一块较大的Eden空间和两块Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
须要注意为啥划分出两个Survivor,主要有两个缘由:一、做为新生代与老年代的缓冲;二、减小空间碎片。另外还要注意的是,在复制的过程当中Survivor的内存空间不够用怎么办?此时老年代会站出来作内存担保。
c、标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操做,效率将会变低。更为关键的是,若是不想浪费50%的空间,就须要有额外的空间进行分配担保。根据老年代的特色(对象存活率较高),有人提出标记-整理算法,标记的含义及理解与前文一致,整理的思想是:让全部存活的对象都向一端移动,而后直接清除掉边界之外的内存。
d、分代收集算法
其实也就是根据新生代老年代的特性分别选择适合的收集算法,新生代因为一半存活对象少选择使用复杂算法进行收集,老年代则使用整理算法进行收集。
什么时候回收?
通常的断定条件为程序具备长时间执行的特性,如方法调用、异常跳转、循环跳转等。
与君共勉
文章主要思想来源于书籍:《深刻理解Java虚拟机》