一.相关概念
GC需要完成三件事:1,哪些内存需要回收?2:什么时候回收?3:如何回收?
Java内存运行时区域的各部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着入栈和出栈操作。
每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,
因为方法结束或者线程结束,内存自然就跟随着回收了。
而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个实现类需要的内存可能不一样,
一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收是动态的,垃圾收集器所关注的是这部分的内存。
因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法!
1、如何判断对象是否存活
在Java中,如何回收对象,第一步,肯定是需要知道对象是否还有引用,是否还存活的。这样,JVM才能进行下一步的操作。
判断对象是否还存活有着如下两种算法:引用计数算法与可达性分析算法。
2、引用计数算法
给每一个对象都添加一个引用计数器,每当有一个地方引用该对象,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法的优点是实现简单,判定效率也高。缺点是它很难解决对象之间相互循环引用的问题。
所以目前主流的虚拟机中都没有使用该算法来管理内存,Java虚拟机当然也并没有使用该算法判断对象是否应该回收。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
代码:
public class abc_test { public static void main(String[] args) { // TODO Auto-generated method stub MyObject object1=new MyObject(); MyObject object2=new MyObject(); object1.object=object2; object2.object=object1; object1=null; object2=null; } } class MyObject{ MyObject object; }
这段代码是用来验证引用计数算法不能检测出循环引用。
最后面两句将object1
和object2
赋值为null
,也就是说object1
和object2
指向的对象已经不可能再被访问,
但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。
3、可达性分析(GC Roots Tracing)算法
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,
继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
采用引用计数算法的系统只需在每个实例对象创建之初,通过计数器来记录所有的引用次数即可。
而可达性算法,则需要再次GC时,遍历整个GC根节点来判断是否回收。(所以引用计数回收的效率比可达性算法效率高)
在Java语言中,可作为GC Roots的对象包括下面几种:
(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中类静态属性引用的对象;
(3)方法区中常量引用的对象;
(4)本地方法栈中JNI(Native方法)引用的对象。
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),
当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
现在问题来了,可达性分析算法会不会出现对象间循环引用问题呢?答案是肯定的,那就是不会出现对象间循环引用问题。GC Root在对象图之外,是特别定义的“起点”,不可能被对象图内的对象所引用。
对象生存还是死亡(To Die Or Not To Die)
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finapze()方法。
当对象没有覆盖finapze()方法,或者finapze()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
程序中可以通过覆盖finapze()来一场"惊心动魄"的自我拯救过程,但是,这只有一次机会。
/** * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个对象的finapze()方法最多只会被系统自动调用一次 */ pubpc class FinapzeEscapeGC { pubpc static FinapzeEscapeGC SAVE_HOOK = null; pubpc void isApve() { System.out.println("yes, i am still apve :)"); } @Override protected void finapze() throws Throwable { super.finapze(); System.out.println("finapze mehtod executed!"); FinapzeEscapeGC.SAVE_HOOK = this; } pubpc static void main(String[] args) throws Throwable { SAVE_HOOK = new FinapzeEscapeGC(); //对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); //因为finapze方法优先级很低,所以暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isApve(); } else { System.out.println("no, i am dead :("); } //下面这段代码与上面的完全相同,但是这次自救却失败了 SAVE_HOOK = null; System.gc(); //因为finapze方法优先级很低,所以暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isApve(); } else { System.out.println("no, i am dead :("); } } }
运行结果为:
finapze mehtod executed!
yes, i am still apve :)
no, i am dead :(
二.Java引用的四种状态
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,
这4种引用强度依次逐渐减弱。
1、强引用(Strong Reference)
强引用一般指的就是new出来的对象(反射出来使用的对象也属于强引用),这是使用最普遍的引用。
只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
2、软引用(Soft Reference)
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。
对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
如下面的代码使用 SoftReference 类来创建软引用。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); obj = null; // 使对象只被软引用关联
3、弱引用(Weak Reference)
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象(也就是说它只能存活到下一次垃圾收集发生之前)。
在java中,用java.lang.ref.WeakReference类来表示。使用 WeakReference 类来实现弱引用。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null; // 使对象只被弱引用关联
java.util中的WeakHashMap通常用来实现缓存,该类中的Entry继承自WeakReference。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。
ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。
eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。
源码如下:
public final class ConcurrentCache<K, V> { private final int size; private final Map<K, V> eden; private final Map<K, V> longterm; public ConcurrentCache(int size) { this.size = size; this.eden = new ConcurrentHashMap<>(size); this.longterm = new WeakHashMap<>(size); } public V get(K k) { V v = this.eden.get(k); if (v == null) { v = this.longterm.get(k); if (v != null) this.eden.put(k, v); } return v; } public void put(K k, V v) { if (this.eden.size() >= size) { this.longterm.putAll(this.eden); this.eden.clear(); } this.eden.put(k, v); } }
4、虚引用(Phantom Reference)
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对对象的生命周期构成影响,也无法通过虚引用取得一个对象实例。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj); obj = null; //使该对象只有虚引用
对于强引用,我们平时在编写代码时经常会用到。而对于其他三种类型的引用,使用得最多的就是软引用和弱引用,这2种既有相似之处又有区别。
它们都是用来描述非必需对象的,但是被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
针对上面的特性,软引用适合用来进行缓存,当内存不够时能让JVM回收内存。弱引用可以用来在回调函数中防止内存泄露。
因为回调函数往往是匿名内部类,隐式保存有对外部类的引用,所以如果回调函数是在另一个线程里面被回调,而这时如果需要回收外部类,那么就会内存泄露,因为匿名内部类保存有对外部类的强引用。
5、什么情况下回收对象
即使是在可行性分析算法中不可达的对象,也并不是立即回收的。至少要经历两次标志过程,才真正宣告该对象"死亡";
可达性分析算法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。
当对象没有覆盖finalize方法,或finalize方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行finalize方法。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非对象在finalize方法中,将对象重新和GC Roots建立了关联,否则就会被回收。
但是,finalize线程的优先级很低,GC并不保证会等待对象执行完finalize方法之后再去回收,因而想通过finalize方法区拯救对象的做法,并不靠谱。
鉴于finalize()方法这种执行的不确定性,建议大家无论什么时候,都不要使用finalize方法。会影响对象回收的性能。且对于对象的回收引入了不确定因素。
7、回收方法区
方法区(或Hotspot虚拟机中的永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常相似。以常量池中字符串的回收为例,若字符串“abc”已经进入常量池中,
但当前系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用该字面量,若发生内存回收,且必要的话,
该“abc”就会被系统清理出常量池。常量池中其他的类(接口)、方法、字段的符号引用与此类似。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是 “无用的类”:
(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
(2)加载该类的ClassLoader已经被回收。
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,此处仅仅是“可以”,而并不是和对象一样(不使用了就必然回收)
当满足上述三个条件的类才可以被回收,但是并不是一定会被回收,需要参数进行控制,例如HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收。
三.垃圾收集算法
垃圾对象如何确定?
Java堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,首先需要确定哪些对象还"活着",哪些已经"死亡",也就是不会被任何途径使用的对象。
1、标记-清除算法(Mark and Sweep)
算法分为“标记”和“清除”阶段。
首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。
图显示了标记清除算法的大致原理。图中的(1)部分显示了随着程序的运行而分配出一些对象的状态,一个对象可以对其他的对象进行引用。
图中(2)部分中,GC开始执行,从根开始对可能被引用的对象打上“标记”。大多数情况下,这种标记是通过对象内部的标志(Flag)来实现的。
于是,被标记的对象我们把它们涂黑。图中(3)部分中,被标记的对象所能够引用的对象也被打上标记。
重复这一步骤的话,就可以将从根开始可能被间接引用到的对象全部打上标记。到此为止的操作,称为标记阶段(Mark phase)。
标记阶段完成时,被标记的对象就被视为“存活”对象。图1中的(4)部分中,将全部对象按顺序扫描一遍,将没有被标记的对象进行回收。这一操作被称为清除阶段(Sweep phase)。
在扫描的同时,还需要将存活对象的标记清除掉,以便为下一次GC操作做好准备。标记清除算法的处理时间,是和存活对象数与对象总数的总和相关的。
缺点:
它是最基础的收集算法,但是会带来两个明显的问题;
(1)标记和清除的过程效率不高(GC 标记 - 清除算法中分块不是连续的,因此每次分配都必须遍历空闲链表,为了找到足够大的分块。最糟的情况就是每次进行分配都得把空闲链表遍历到最后)
(2)空间问题(在 GC 标记 - 清除算法的使用过程中会逐渐产生被细化的分块,不久后就会导致无数的 小分块散布在堆的各处。
如果发生碎片化,那么即使堆中分块的总大小够用,也会因为一个个的分块都太小而不能执行分配。)
(3)需要使用空闲链表 (free.list),来记录所有的空闲区域,以及每个区域的大小。维护空闲表增加了对象分配时的开销。
(4)与写时复制技术不兼容
写时复制:
写时复制(copy-on-write)是众多 UNIX 操作系统用到的内存优化的方法。比如在 Linux 系统中使用 fork() 函数复制进程时,
大部分内存空间都不会被复制,只是复制进程,只有在内存中内容被改变时才会复制内存数据。
但是如果使用标记清除算法,这时内存会被设置标志位,就会频繁发生不应该发生的复制。
另外,关于标记清除的变形,还有一种叫做标记压缩(Mark and Compact)的算法,它不是将被标记的对象清除,而是将它们不断压缩。
2、复制收集算法(Copy and Collection)
标记清除算法有一个缺点,就是在分配了大量对象,并且其中只有一小部分存活的情况下,所消耗的时间会大大超过必要的值,这是因为在清除阶段还需要对大量死亡对象进行扫描。
复制收集(Copy and Collection)则试图克服这一缺点。在这种算法中,会将从根开始被引用的对象复制到另外的空间中,然后,再将复制的对象所能够引用的对象用递归的方式不断复制下去。
简单的说:它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
这样就使每次的内存回收都是对内存区间的一半进行回收。
内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
图二中的(1)部分是GC开始前的内存状态,这和图一的(1)部分是一样的。
图二的(2)部分中,在旧对象所在的“旧空间”以外,再准备出一块“新空间”,并将可能从根被引用的对象复制到新空间中。
图中(3)部分中,从已经复制的对象开始,再将可以被引用的对象像一串糖葫芦一样复制到新空间中。复制完成之后,“死亡”对象就被留在了旧空间中。
图中(4)部分中,将旧空间废弃掉,就可以将死亡对象所占用的空间一口气全部释放出来,而没有必要再次扫描每个对象。下次GC的时候,现在的新空间也就变成了将来的旧空间。
通过图二我们可以发现,复制收集方式中,只存在相当于标记清除方式中的标记阶段。由于清除阶段中需要对现存的所有对象进行扫描,
在存在大量对象,且其中大部分都即将死亡的情况下,全部扫描一遍的开销实在是不小。而在复制收集方式中,就不存在这样的开销。
但是,和标记相比,将对象复制一份所需要的开销则比较大,因此在“存活”对象比例较高的情况下,反而会比较不利(极端情况下,实际使用的内存效率只有50%)。
优点:
(1)优秀的吞吐量
GC标记-清除算法消耗的吞吐量是搜索活动对象(标记阶段)所花费的时间和搜索整体堆(清除阶段)所花费的时间之和。
而GC复制算法只搜索并复制活动对象,所以跟一般的GC标记-清除算法相比,它能在短时间内完成GC,也就是说其吞吐量优秀。
尤其是堆越大,差距越明显。GC标记-清除算法在清除阶段所花费的时间会不断增加,但GC复制算法就不会。因为G标记-清除算法消耗的时间是与活动对象的数量成比例的。
(2)可实现高速分配
GC复制算法不使用空闲链表,这是因为分块是一块连续的内存空间。因此,只要这个分块大小不小于所申请的大小,那么移动指针就可以进行分配了。
比起GC标记-清除算法和引用计数算法等使用空闲链表的分配内存空间,GC复制算法明显快得多。
遍历空闲链表是为了找到满足要求的分块,最坏的情况是我们不得不从空闲链表中取出最后一个分块,这样就用了大量时间把所有分块都调查一遍。
(3)不会发生碎片化
基于算法性质,活动对象被集中安排在新空间的开头。像这样把对象重新集中,放在堆中一端的行为叫作压缩。在GC复制算法中,每次运行GC时都会执行压缩。
因此GC算法有个非常优秀的特点,就是不会发生碎片化,也就是说可以安排分块允许范围内大小的对象。
(4)与缓存兼容
复制算法具有局部性(Lo-cality)。在复制收集过程中,会按照对象被引用的顺序将对象复制到新空间中。
于是,关系较近的对象被放在距离较近的内存空间中的可能性会提高,这被称为局部性。
在局部性高的情况下,内存缓存会更容易有效运作,程序的运行性能也能够得到提高。
缺点:
(1)堆使用率低下
GC复制算法把堆分成二等分,通常只能利用其中一半来安排对象。也就是说只有一半堆能被使用,相比其他能使用整个堆的GC算法而言,这是GC复制算法的一个重大缺陷。
详细介绍:
现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,
而是将内存分为一块比较大的Eden Space(伊甸园)空间和两块较小的Survivor Space(幸存者区)空间,每次使用Eden和其中一块Survivor。
当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot虚拟机(Java虚拟机的一个实现)默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),
只有10%的空间会被浪费(也就是说,至少会浪费10%的空间)。
当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。
(2)不兼容保守式GC算法
GC标记-清除算法有着跟保守式GC算法相兼容的优点。因为GC标记-清除算法不用移动对象。
另一方面,GC复制算法必须移动对象重写指针,所以有着跟保守式GC算法不相容的性质。虽然有限制条件,GC复制算法和保守式GC算法可以进行组合。
保守式GC:
简单的说,就是不能识别指针和非指针的GC
把不能识别指针还是非指针的对象当做指针来保守处理,也就是当成活动对象保留下
(3)递归调用函数
在算法中,复制某个对象时要递归复制它的子对象,因此在每次进行复制的时候都要调用函数,由此带来的额外负担不容忽视。
比起递归算法,迭代算法更能有效地执行。(迭代算法是用计算机处理问题的一种基本方法。它利用计算机运算速度快、适合做重复性操做的特点,
让计算机对一组指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,都从变量的原值推出它的一个新值。
程序调用本身的编程技巧称为递归( recursion)。
一个过程或函数在其定义或说明中又间接或间接调用本身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题类似的规模较小的问题来求解,
递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。用递归思想写出的程序往往十分简洁易懂。)
此外,因为在每次递归调用时都会消耗栈,所以还有栈溢出的可能。
3、标记-整理算法
复制算法对于对象存活率很低的情况是高效的,但是当对象的存活率非常高时,就变得非常低效了。
在老年代中,对象的存活率很高,所以不能使用复制算法。于是根据老年代的对象特点,提出了标记-整理(Mark-Compact)算法。
标记-整理算法也分为两个阶段:标记和整理。
第一个阶段与标记-清除算法一样:标记出所有可以被回收的对象。
第二个阶段不再是简单的清除无用对象的空间,而是将后面的活着的对象依次向前移动。
将所有的活着的对象都移动成内存空间中前段连续一个区域,之后的连续的区域都是可分配的没有使用的内存空间。
优点:不会产生内存碎片。
缺点:在标记的基础之上还需要进行对象的移动,成本相对较高,效率也不高。
4、垃圾收集算法比较
相同点:
(1)三个算法都基于根搜索算法去判断一个对象是否应该被回收,而支撑根搜索算法可以正常工作的理论依据,就是语法中变量作用域的相关内容。
因此,要想防止内存泄露,最根本的办法就是掌握好变量作用域。
(2)在GC线程开启时,或者说GC过程开始时,它们都要暂停应用程序(stop the world)。
比较:
(1)效率:复制算法 > 标记/整理算法 > 标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
(2)内存整齐度:复制算法=标记/整理算法>标记/清除算法。
(3)内存利用率:标记/整理算法=标记/清除算法>复制算法。(>表示前者要优于后者,=表示两者效果一样)
注1:标记-整理算法不仅可以弥补标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
注2:可以看到标记/清除算法是比较落后的算法了,但是后两种算法却是在此基础上建立的。
注3:时间与空间不可兼得。
5、分代收集算法
当前商业虚拟机中一般采用“分代收集算法”。分代收集算法是根据对象的特点将内存空间分成不同的区域(即不同的代),对每个区域使用合适的收集算法。
在JVM中一般分为新生代和老年代,新生代中对象的存活率比较低,使用复制算法简单高效;在老年代中,由于对象的存活率较高,所以一般采用标记-整理算法。
简单的理解:
(1)存活率低:少量对象存活,适合复制算法
(2)存活率高:大量对象存活,适合用标记-清理/标记-整理算法
注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次GC都没有被回收掉而进入老年代。
总的来说,分代收集算法并不是一种具体的算法,而是根据每个年龄代的特点,多种算法结合使用来提高垃圾回收效率。
6、小结
没有最好的算法,只有最适合的算法。
每一种算法的存在必然有其使用场景和适合的地方。