这里主要是阐明各算法的实现思想,而不去细论算法的具体实现
标记—清除算法(Mark-Sweep)
标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析算法中判定垃圾对象的标记过程。标记—清除算法的执行情况如下图所示:
- 回收前状态
- 回收后状态
该算法有如下缺点:
-
标记和清除过程的效率都不高
-
标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作
复制算法(Copy)
复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:
-
每次只对一块内存进行回收,运行高效
-
只需移动栈顶指针,按顺序分配内存即可,实现简单
-
内存回收时不用考虑内存碎片的出现
它的缺点是:可一次性分配的最大内存缩小了一半
复制算法的执行情况如下图所示:
- 回收前状态
- 回收后状态
现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。
当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。
标记—整理算法(Mark-Compact)
复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:
- 回收前状态:
- 回收后状态:
分代收集
当前商业虚拟机的垃圾收集都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代。
-
在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集
-
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收
GC算法 | 优点 | 缺点 | 存活对象移动 | 内存碎片 | 适用场景 |
---|---|---|---|---|---|
引用计数 | 实现简单 | 不能处理循环引用 | |||
标记清除 | 不需要额外空间 | 两次扫描,耗时严重 | N | Y | 老年代 |
复制 | 没有标记和清除 | 需要额外空间 | Y | N | 新生代 |
标记整理 | 没有内存碎片 | 需要移动对象的成本 | Y | N | 老年代 |
补充:
增量算法 (Incremental Collecting)
在垃圾回收过程中,应用软件将处于一种 CPU 消耗很高的状态。在这种 CPU 消耗很高的状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。
增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
深入理解分代收集算法
1. 为什么要分代
分代的垃圾回收策略,是基于这样一个事实:不同对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。还有一些对象主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
分代的好处:如果单纯从JVM的功能考虑(用简单粗暴的标记-清理删除垃圾对象),并不需要新生代,完全可以针对整个堆进行操作,但是每次GC都针对整个堆标记清理回收对象太慢了。把堆划分为新生代和老年代有2个好处:
- 简化了新对象的分配(只在新生代分配内存)
- 可以更有效的清除不再需要的对象(死对象)。
在新生代中,GC可以快速标记回收“死对象”,而不需要扫描整个堆中的存活一段时间的“老对象”。
2. 什么情况下触发垃圾回收
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GC和Full GC。
-
Minor GC(新生代回收)的触发条件比较简单,Eden空间不足就开始进行Minor GC回收新生代。
-
Full GC(老年代回收,一般伴随一次Minor GC)则有几种触发条件:
-
(1)老年代空间不足
-
(2)PermSpace空间不足
-
(3)统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
这里注意一点:PermSpace并不等同于方法区,只不过是Hotspot JVM用PermSpace来实现方法区而已,有些虚拟机没有PermSpace而用其他机制来实现方法区。
-
补充:对象的空间分配和晋升
(1)对象优先在Eden上分配
(2)大对象直接进入老年代
虚拟机提供了-XX:PretenureSizeThreshold参数,大于这个参数值的对象将直接分配到老年代中。因为新生代采用的是复制算法,在Eden中分配大对象将会导致Eden区和两个Survivor区之间大量的内存拷贝。
(3)长期存活的对象将进入老年代对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会晋升到老年代中。
3. 为什么不是一块Survivor空间而是两块?
这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历15次(仅供参考)GC,就可以移到老年代了。问题来了,当我们第一次GC的时候,我们可以把Eden区的存活对象放到Survivor A空间,但是第二次GC的时候,Survivor A空间的存活对象也需要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。
所以,这里就需要两块Survivor空间来回倒腾。
4. 为什么Eden空间这么大而Survivor空间要分的少一点?
新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不多。所以,设置较大的Eden空间和较小的Survivor空间是合理的,大大提高了内存的使用率,缓解了Copying算法的缺点。
我看8:1:1就挺好的,当然这个比例是可以调整的,包括上面的新生代和老年代的1:2的比例也是可以调整的。
新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。
5. Eden空间和两块Survivor空间的工作流程
这里本来简单的Copying算法被划分为三部分后很多朋友一时理解不了,也确实不好描述,下面我来演示一下Eden空间和两块Survivor空间的工作流程。
现在假定有新生代Eden,Survivor A, Survivor B三块空间和老生代Old一块空间。
// 分配了一个又一个对象
放到Eden区
// 不好,Eden区满了,只能GC(新生代GC:Minor GC)了
把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区
// ...
// 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程中突然发现:
// 不好,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)
Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...
6. Java 垃圾回收会出现不可回收的对象吗?
内存泄露问题