本节为JVM垃圾收集的基础理论,一个GC过程在逻辑上须要通过两个步骤,即先判断哪些对象是存活的、哪些对象是死亡的,而后对死亡的对象进行回收。
1、关于回收目标
在前面咱们已经了解到,JVM的内存模型划分为多个区域,因为不一样区域的实现机制以及功能不一样,那么各自的回收目标也不一样。通常来讲,内存回收主要涉及如下三个区域:
- 虚拟机栈/本地方法栈:顾名思义,该部份内存以栈的形式做为实现,那么在进栈、出栈的时候内存会自动释放,相似于C的“自动变量区域内存”;
- 堆:内存回收主要目标,能够认为相似于C中的“动态内存分配区域”,只不过C经过malloc与free函数手动进行管理,而java经过GC进行自动管理;
- 方法区:该区域回收效果很弱,虚拟机规范强制要求在这里进行回收。回收目标是常量池的回收和对类型的卸载;
2、方法区回收
方法区的回收目标是回收常量池中的废弃常量与类卸载。
2.1.常量回收
若常量池中的某常量没有任何地方引用或者使用,包括该常量不以字面量的形式被使用或引用,则能够被回收。
2.2.类卸载
知足如下条件的类能够被卸载:
- 该类全部实例已被回收;
- 该类的ClassLoader已被回收;
- 该类的类型信息,即java.lang.Class没有任何地方引用(通常为反射使用);
能够看出,对于类的卸载,要求很苛刻。所以在大量使用反射、动态代理、CGLib等字节码框架、动态生成jsp以及OSGI这类频繁自定义ClassLoader功能的场景中,都要求JVM具有类卸载功能,以保证永久带不溢出。
3、堆回收
3.1.对象存活断定
关于堆中的对象存活断定,以标记为基础,并配合其余步骤完成。
3.1.1.标记算法
(1)引用计数法
即给对象添加一个引用计数器,每有一个地方进行引用,则计数器加1。当计数器为0的时候,表示该对象可回收。
引用计数法未被JVM采用,缘由是其没法解决对象间循环引用的问题,以下图所示,当堆内的两个对象循环引用,就算他们已经没用了,也没法进行回收:
(2)可达性分析算法
该算法的思想是将一系列被称为“GC ROOTS”的对象做为起点(或称根节点),向下搜索,所走过的路径称为“引用链(reference chain)”。若一个对象没有能够到达GC ROOTS的路径,则称“该对象不可达”。对于不可达对象,会被标记为回收状态。
上图中,顺着GC ROOTS,Obj一、Obj二、Obj3和Obj4都是能够到达的,所以他们为存活对象;而Obj5不可到达,Obj六、Obj7即便存在指向它们的引用,但因没法到达GC ROOTS,所以为须要回收的对象。
在可达性分析算法中,最重要的就是GC ROOTS。其本质是对象,但并不是全部对象都有资格做为GC ROOTS,只有如下位置的才能够:
- 栈上引用:虚拟机栈的栈帧中本地变量表内引用的对象;
- 栈上引用:本地方法栈中JNI引用的对象;
- 方法区:类静态属性引用的对象;
- 方法区:类常量引用的对象;
3.1.2.死亡断定
对象在通过标记以后,并不会立刻被回收,还要通过如下一系列阶段才最终肯定须要被回收:
- 一次标记:即经过标记算法将对象标记为待回收状态,并进入一个待回收对象集合;
- 筛选:对一次标记以后的待回收对象进行过滤,若是该对象覆盖了finalize方法,而且该方法未执行过,则将该对象放入F-QUEUE;反之,对象没有覆盖finalize方法或者finalize方法已经被执行过了,该对象不会进行任何处理;
- F-QUEUE:一个队列,JVM会经过一个Finalizer线程去执行这个队列中对象的finalize方法,而且只保证该方法的执行,不保证该方法成功执行完成。由于若finalize方法有死循环,会形成FQUEUE后续未被执行对象的持续等待,致使整个内存回收系统崩溃。根据这个特色,对象能够在执行finalize方法时进行“自救”,所谓的自救,就是将对象从新与GC ROOTS相关联;
- 二次标记:GC会对FQUEUE中的对象进行额外的一次标记,若对象“自救”成功,则会从待回收对象集合中移除;若对象“自救”失败,它仍然会处于待回收对象集合中,等待真正被回收;
- 回收:对象经过垃圾收集进行回收,释放内存空间;
3.2.垃圾收集算法
在上一小节咱们讲了对象标记相关的算法,本小节来了解一下垃圾收集算法。
3.2.1.标记-清除算法
标记-清除(mark-sweep)算法,是最基础的垃圾收集算法,它的思想比较简单,就是在“对象存活断定”标记出须要回收的对象后,统一回收(清除)这些对象的内存。
该算法简单有效,可是存在两个不足:首先是效率问题,标记和清除两个阶段的效率都不高,所谓效率不高,并不是指的是自身的执行效率,而是指回收结果与耗时的效益比不高;其次是空间问题,标记-清除算法并未整理内存,会产生大量不连续的内存碎片,要分配较大对象时,可能没法找到足够的连续内存而不得不又触发一次GC。
3.2.2.复制算法
复制算法(copying)是对标记-清除算法的改进,其主要思想是将内存划分为不一样的区域,包括“内存使用区”和“结果缓冲区”。每次只使用一部份内存,在该部份内存满了以后,将仍然存活的对象复制到另一块区域上面,而后将以前使用过的内存区域所有清理掉,
现代商业虚拟机都采用其回收新生代。
该算法大大提升了回收效率,也能够避免内存碎片。然而带来了新的问题:因为须要开辟一块内存空间做为每次回收结果的缓冲,所以可用内存没法达到100%,“结果缓冲区”的大小决定了内存有效的比率。
如何设置结果缓冲区的内存大小(比例)?将其设置为50%最能确保每次回收都有足够大小的缓冲区域存放回收结果,毕竟最差的状况就是全部对象都存活,然而内存浪费也过高了。根据IBM的研究,通常状况下,新生代中的对象98%都是“朝生夕死”的,也就是说,每次存活对象的比例并不会过高,咱们只须要设置一小块内存做为“回收结果缓冲”便可,他们提出的解决模型以下,将内存划分为eden与2块suvivor:
- eden:主存储区,新对象的建立都在这块区域;
- survivor:分为两块,一块做为上次回收结果的“缓存”,一块做为下一次回收的“缓存”区域;
基于这种模型,每次回收时,将eden和上次回收结果的survivor中存活的对象复制进空闲的survivor,而后清理掉被回收的区域便可,简单的示意流程图见下:
值得注意的是,对于eden-survivor模型,98%的对象可回收只是理想理论,在某些场景下,回收时存活对象的大小有可能大于空闲survivor。对于这种survivor空间大小不够用的状况,须要经过“分配担保”机制来保证对象能正确留存。所谓的分配担保,就是不够空间survivor存放的对象进入老年代。
3.2.3.标记-整理算法
在上一小节咱们知道复制算法主要适合于新生代的回收,对于老年代这种对象存活率高的区域,由于每次都会复制大量对象,成本收益比较低,使用复制算法明显不合适;相反,标记-清除算法更适合老年代的特征,为了解决标记-清除算法的内存碎片问题,在此基础上,优化为标记-整理算法(mark-compact)。
标记-整理算法主要思想是在标记对象后,将存活对象向内存的一端移动,而后清理掉端边界之外的内存,所谓的整理也能够理解为压缩。
3.2.4.总结
没有哪种垃圾收集算法可以适用于全部状况,对于不一样的堆内存区域(新生代、老年代),须要根据实际的对象特征,选择合适的算法。
算法 |
优势 |
缺点 |
适用区域 |
复制 |
效率较高,无内存碎片问题 |
1.内存利用率达不到100%;2.须要分配担保机制确保对象存活率较高时的内存分配; |
新生代(对象存活率低,复制成本低) |
标记-清除 |
简单有效 |
1.效率不高;2.有内存碎片问题; |
老年代(对象存活率高,无额外空间进行分配担保) |
标记-整理 |
标记-清除的改良,解决了内存碎片问题 |
1.一样存在效率问题;2.整理过程须要额外的时间开销; |