垃圾回收机制及算法
垃圾回收基础
什么是GC
java 相比较C++等语言最大的区别就是,java是自动化的垃圾回收机制(GC)
栈:栈中的生命周期是跟随线程,所以一般不需要关注。
堆:堆中的对象是垃圾回收的重点。
方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是回收的重点。
分代回收理论
当前商业虚拟机的垃圾回收器,大多遵循分代收集的理论来进行设计。
理论描述:
1、绝大多数的对象都是朝生夕死。
2、熬过多次垃圾回收的对象就越难回收。
根据上述理论,朝生夕死的对象放在一个区域,难回收的对象放在一个区域,这就构成了新生代和老年代。
GC 的分类
目前发生垃圾回收的叫法很多,大致如下:
1、新生代回收(Minor GC/Young GC):只进行新生代的回收。
2、老年代回收(Major GC/Old GC):只进行老年代的回收,目前只有CMS垃圾回收器会有这个单独的回收老年代的行为。(Major GC 定义是比较混乱,有说指是老年代,有的说是做整个堆的收集,这个需要你根据别人的场景来定,没有固定的说法)。
3、整堆回收(Full GC):收集整个java堆和方法区。
垃圾回收算法(思想)
复制算法(Copying)
将可用的内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将存活着的对象复制到另一块内存区域,然后再把已使用过的这块空间一次清理掉(格式化),这样使得每次都是整个搬去进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按照顺序分配内存即可。
特点:
1、实现简单、运行高效。
2、没有内存碎片。
3、利用率只有一半。
注意:内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整。
复制算法适合新生代,因为新生代的对象大部分朝生夕死,那么复制过去的对象比较少,效率自然就高,另一半的一次清清理是很快的。
Appel 式回收
一种更加优化的复制回收分代策略:具体做法是分配一个较大的Eden区和两块较小的Survivor空间(From和To区)。
研究表明,新生代中的对象98%是朝生夕死的,所以不需要按照1:1的比例来划分空间,而是将内存分为一块较大的Eden区和两个较小的Survivor空间,每次使用Eden区和其中一块Survivor区,当回收时,将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和之前的那个Survivor空间。
HotSpot虚拟机默认Eden 和Survivor大小比例是8:1,也就是每次能使用的占整个空间的90%,只有10%的内存会被浪费。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要 依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
标记-清除算法(Mark-Sweep)
算法分为“标记”和清除两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低,所以标记清除算法适用于老年代。
存在问题:会产生大量不连续的内存碎片,空间碎片太多可能会当值在程序运行过程中需要分配大对象时,无法找到足够连续的内存而不得不提前触发另一次垃圾回收动作。
特点:
1、效率略低
2、位置不连续,产生碎片
3、两遍扫描
标记-整理算法(Mark-Compact)
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端 边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。 我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用 对象的地方都需要更新(直接指针需要调整)。 所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。
特点:
1、没有内存碎片
2、效率偏低
3、两边扫描、指针需要调整。