Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进来,墙里面的人却想出来.
今天就来简单总结一下Java虚拟机中的垃圾收集机制.
概述
在介绍GC机制之前,先来说一个小问题.
也许是因为惯性思维吧,导致我一听到垃圾收集,就会不由自主的联想到JVM,大部分人应该也是这样吧,但其实这里要给大家纠正一下,垃圾收集技术其实并不是Java这门语言的伴生产物,事实上,GC的历史比Java语言还要久远.1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言.
GC需要做什么?
1,WHAT?
哪些内存需要回收?
2,WHEN?
什么时候回收?
3,HOW?
如何回收?
目前内存的动态分配与内存回收技术已经很成熟了,在如此自动划的时代,我们为什么要去费这么大劲去了解他们呢?这也正呼应了文章开头的那句经典的话,Java程序员把内存控制的大权交给了Java虚拟机,所以一旦出现内存泄漏和溢出方面的问题,如果你不了解虚拟机是怎样使用内存的,那么排查错误就成了一项极其困难的工作.
当需要排查各种内存溢出,内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些"自动化"技术实施必要的监控和调节.
对象已死吗?
垃圾收集器在对堆进行回收前,第一件事就是确定哪些对象还存活着?哪些对象已经死去?死去也就是说这个对象已经不可能再被任何途径使用.
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,当引用失效时,计数器值减1,任何时刻计数器值为0的对象就是不可能再被使用的.
怎么说呢,引用技术算法对于我们来说,容易理解,而且实现起来也很简单,判定效率也挺高的,综合来说,大部分情况下它都可以称的上是一个很不错的算法,但是,主流的Java虚拟机里面却没有选用它来管理内存,最主要的一个原因就是引用计数算法很难解决对象之间相互循环引用的问题.
例如:
对象A,对象B都有字段values,赋值令A.value = B;
B.value = A; 除此之外,它们之间再无任何引用,这两个对象在实际上也不能再被访问了,但是它们因为相互引用着对方,导致它们的引用计数都不为0,这样的话,引用计数方法无法通知GC收集器回收它们.
可达性分析算法
可达性分析算法的基本思路是通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的,所以将会被判定为是可回收的对象.
Java中可以作为GC Roots的对象包括:
1,虚拟机栈(栈帧中的本地变量表)中引用的对象
2,方法区中类静态属性引用的对象
3,方法区中常量引用的对象
4,本地方法栈中JNI(Native方法)引用的对象
引用
我感觉引用一直以来是一个很模糊的概念,知道它是什么,却又很难清晰的表达本质含义,现在,我们来谈谈引用.
前面介绍判定对象是否存活的两种算法都与"引用"有关.传统的引用定义太过片面话了,一个对象在传统的引用定义下只有被引用和没有被引用这两种状态,可是这样定义的话,你该怎么去描述下面这种对象呢?当内存空间还足够时,则能保留在内存之中,如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这类对象.
针对这些,JDK1.2之后,Java对引用的概念进行了扩充,分为强引用,软引用,弱引用,虚引用.这四种引用强度依次逐渐减弱.
A.强引用---指在程序代码之中普遍存在的,类似于"Object obj = new Object"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象.
B.软引用---描述的是一些还有用但并非必需的对象.对于软引用关联的对象,在系统即将发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收.如果这次回收还没有足够的内存,才会抛出内存溢出异常.(SoftReference类实现软引用)
C.弱引用---描述非必需对象,她的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前.当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象.(WeakReference类实现弱引用)
D.虚引用---幽灵引用或幻影引用,它是最弱的一种引用关系.一个对象是否有虚引用的存在,完全不会对其生存空间构成影响,也无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知.(PhantomReference类来实现虚引用)
生存还是死亡
要真正宣告一个对象死亡,至少要经历两次标记过程.
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那他将被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要进行finalize()方法.
1,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用,虚拟机将这两种情况都视为"没有必要执行".
2,如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个F-Queue的队列中,并在稍后由一个由虚拟机自动建立的,低优先级的Finalizer线程去执行它.
finalize()方法是对象逃脱死亡的最后一次机会.
稍后GC会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己---只要重新与引用链上的任何一个对象建立关联即可,那么在第二次标记时它将会被移除出"即将回收"的集合.
特别要强调的是:任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行.
关于对象死亡时finalize()方法,不太建议使用,因为它的运行代价高昂,不确定性大,无法保证每个对象的调用顺序.对于finalize()能做的所有工作,使用try-finally或者其他方式都可以做的更好,更及时.
回收方法区
方法区(永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类.
无用的类
该类所有的实例都已经被回收,Java堆中不存在该类的所有实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.CLass对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法
标记-清除算法
标记-清除算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象.
主要不足:1,效率问题,标记和清除两个过程的效率都不高.
2,空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续的连续内存而不得不提前触发另一次垃圾收集动作.
复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块.当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后将已使用的内存空间一次清理掉.这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配即可,实现简单,运行高效.
只是复制算法是将内存缩小为原来的一半,代价太高.
现在的商业虚拟机都使用赋复制算法来回收新生代,因为新生代中的对象大部分都是"朝生夕死",所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清除掉Eden和刚才使用过的Survivor空间.
当然,并不能保证每次回收都只有不多于Survivor空间大小的对象存活,所以当Survivor空间不够用时,需要依赖其他内存进行内存分配担保.
内存分配担保机制
说到内存分配担保机制,这里我来用银行贷款的例子来形容一下,如果你在银行的信誉一直很好,在98%的情况下都能按时偿还贷款,那么银行就会默认你下一次仍然能够按时按量偿还贷款,你只需要一个担保人来向银行保证如果你不能按时按量还款时,可以从担保人的账户扣钱,这样银行就可以规避风险.内存分配担保的道理和它一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代.
标记-整理算法
根据老年代的特点,有人提出了标记-整理算法,标记操作完成之后,让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存.
分代收集算法
分代收集算法根据对象存活周期的不同将内存划分为几块.一般把java堆分为新生代和老年代,然后根据年代特点去采用适当的收集算法
1.新生代:每次垃圾收集时都会发现大批对象死去,只有少量存活,选用复制算法
2.老年代:对象存活率高,没有额外空间对其进行分配担保,必须使用"标记-清理"或"标记-整理"算法进行回收