JVM 垃圾回收机制
JVM 垃圾回收机制主要考虑:回收依据 以及 如何回收
一、垃圾回收判定
目前主要有两种算法来标识对象是否可以回收:引用计数法 和 可达性分析算法,目前主流的JVM(HotSpot)采用的是第二种
1、引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
但是,至少主流的Java虚拟机里面没有选用引用技术算法来管理内存,其中最主要的原因是它很难解决对象之间相 互循环引用的问题。例如,对象objA和objB都有字段instance,赋值令objA.instance = objB 及 objB.instance = objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。
2、可达性分析算法
在主流的商用程序语言(Java、C#等)的主流实现中都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的成为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈中引用的对象。(栈贞中的本地变量表)
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象。(即一般说的Native方法)
生死抉择
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
第一次标记:对于一个没有其他引用的对象,筛选该对象是否有必要执行finalize()方法,如果没有执行必要,则意味可直接回收。(筛选依据:是否复写或执行过finalize()方法;因为finalize方法只能被执行一次)。
第二次标记:如果被筛选判定位有必要执行,则会放入FQueue队列,并自动创建一个低优先级的finalize线程来执行释放操作。如果在一个对象释放前被其他对象引用,则该对象会被移除FQueue队列。
二、垃圾收集算法
1、标记-清除 算法(Mark-Sweep)
- 一个是效率问题,标记和清除两个过程的效率都不高
- 另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另外一次手机动作。
2、复制算法
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
HotSpot中默认采用3个区域,分别为:Eden、supervisor1(From)、supervisor2(To),对应的Eden:supervisor=8:1。
为什么是这个比例呢,是这样考虑的:由于新生代中的对象98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和 刚才用过的Survivor空间。但是我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保。
3、标记整理算法
总结
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为 年轻代、老年代、持久代,对不同生命周期的对象使用不同的算法进行回收。现在的垃圾回收器是使用此算法的。