持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情
一. GC算法
1.1 标记-清除算法(Mark-Sweep)
算法分为“标记”和“清除”两个阶段首先标记出所有需要回收的对象,然后回收所有需要回收的对象。
问题:效率不高,需要扫描所有的对象,堆越大,GC越慢,并且存在严重的内存碎片问题,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作,GC次数越多,碎片越严重。
示例如下,从GCROOT开始找到存活的对象,红色的就是未被标记要回收的,并且红色的区域被回收之后,绿色的还在“原地”,并不会对内存区域进行整理。
1.2 标记-整理算法(Mark-Compact)
标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。
1.3 复制搜集算法(Coping)
以前的复制收集算法
描述:将可用的内存分为两半, 每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另一块上,然后就把原来整块内存空间清理
问题:这样使每次内存回收都是对整个半区的回收,内存分配时就不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效,只是这种算法将堆空间内存缩为原来的一半。
现在的复制搜集算法
描述:现在的商业虚拟机都是采用复制搜集算法来回收新生代,将内存分为一块较大的eden空间和两块较小的survivor空间,每次只是用eden和其中一块survivor空间,当回收时将eden和survivor空间中还存活的对象一次性拷贝到另一个survivor空间上,然后清理用过的eden和survivor空间,oracle hotspot虚拟机默认eden 和 survivor的比例是 8:1 ,也就是每次只有百分之十的内存被浪费。示例图如下(最开始A被引用,A引用了C,C引用了H,GC的最后清除了D和G)
好处:1.只需要扫描存活的对象,效率更高;2.不会产生碎片 3.复制算法非常适合对象存活时间比较短的对象,因为每次GC总能回收大部分的对象,复制的开销比较小。根据IBM的专门研究,98%的Java对象只会存活1个GC周期,对这些对象很适合用复制算法。而且不用1:1的划分工作区和复制区的空间
问题:复制搜集算法在对象存活率高得时候效率有所下降,就需要有额外的空间进行分配担保用于应付内存中所有对象都百分之百存活的极端情况(在新生代中可以使用老年代进行空间分配担保),所以在老年代不能直接采用这种算法(不是很懂)
1.4 分代算法(Generational)
描述:当前商业虚拟机的垃圾收集都是采用“分代收集”( Generational Collecting)算法根据对象不同的存活周期将内存划分为几块;一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,1. 譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集 2. 并且有老年代作为空间分配担保;老年代采用Mark- Sweep或者Mark- Compact算法
年轻代( Young Generation)新生成的对象都放在新生代。年轻代用复制算法进行GC(理论上年轻代对象的生命周期非常短,所以适合复制算法,因为大部分都是不存活的对象),年轻代分三个区,一个Eden区,两个 Survivor区(可以通过参数设置 Survivor个数)。对象在Eden区中生成,在新生代垃圾回收时,Eden区和From Survivor区中还存活的对象将被复制到另一个 Survivor区(称为To Survivor区),此次垃圾回收完成之后From Survivor和To Survivor区交换角色。下一次垃圾回收时重复上述过程,直到To Survivor 区被填满,然后一次性将To Survivor中的所有对象移动到老年代中。2个 Survivor是完全对称,轮流替换。Eden和2个 Survivor的缺省比例是8:1:1,也就是10%的空间会被浪费。可以根据 GC log的信息调整大小的比例
老年代( Old Generation)存放了经过一次或多次GC还存活的对象般采用Mark- Sweep或者Mark- Compact算法,进行GC有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个GC算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器(追求吞吐量?追求最短的响应时间?)