常用的垃圾收集算法
1. 标记-清除算法
标记 - 清除算法采用从根集合(GC Roots)进行扫描,对存活对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象。进行回收,可以参照下图,标记-清除算法不需要进行对象的移动,只需要对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但是由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
2. 复制算法
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时吧堆分为一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集合(GC Roots)中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。优点实现简单高效,没有碎片,缺点空间利用率比较低
3. 标记 - 整理算法
标记整理算法采用标记-清除算法一样的方式进行对象的标记,但清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记 - 整理算法是在标记 - 清除算法的基础上,有进行了对象的移动,因此成本更高,但是解决了内存碎片的问题。如下图:
4. 分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法,核心思像是根据对象存活的生命周期将内存划分为若干不同的区域。一般情况下将堆区分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最合适的收集算法
关于 年轻代(Young Generation)的回收算法
(1)所有新生成的对象首先都放在年轻代的,年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
(2)新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将Eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
(3)当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
(4)新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
年老代(Old Generation)的回收算法
(1)在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
(2)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(Permanent Generation)的回收算法
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。
方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
- 加载该类的
ClassLoader
已经被回收; - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。