九又四分之三站台

0%

GC算法(补充)

概述

垃圾收集(Garbage Collection, GC), 诞生于1960年 MIT 的 Lisp 语言。

程序计数器,虚拟机栈,本地方法栈3个区域岁线程而生,随线程而灭;栈中的栈桢随着方法的进入和退出而有条不紊地执行者出栈和入栈的操作,实现了内存的自动清理。因此,我们的内存垃圾回收主要集中于java堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

对象已死?

如何判断一个对象是否存活

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,引用失效时计数器值就减1,当计数器值为0时就可以回收。 但是这种方法无法解决循环引用的问题

可达性分析(Reachability Analysis)

从GC roots的对象开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC roots没有任何引用链可以到达时,该对象就是不可用的

在Java语言中,GC roots包括

  1. 虚拟机栈(栈桢中的本地变量表)表引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(Native方法)引用的对象

垃圾收集器算法

标记-清除算法

算法分为“标记”,“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点: 一是效率问题,标记和清除两个过程的效率都不高;二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致在需要分配较大对象时无法找到足够的连续内存而不得不提前触发一次垃圾收集

复制算法

“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

标记-压缩算法

复制算法在对象存活率比较高时要执行较多的复制操作,效率会降低,而且空间使用率较低,在老年代中一般不能直接使用。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,“标记”过程与“标记-清除”算法的“标记”过程一样,但后续操作不是清除,而是让所有存活的对象往一端移动,然后清理端边界以外的内存。

分代收集算法

描述

GC分代的基本假设:绝大多数的对象生命周期都非常短暂

“分代收集”(Generational Collection)算法,与其说是一种算法,倒不如说是一种选择,一种策略。“分代收集”把Java堆分为新生代和老年代,这样可以根据各个年代的特点分别采取最适合的垃圾收集算法。新生代中每次垃圾收集时都有大量对象死去,采用“复制”算法。老年代中对象生存率较高,没有额外空间对它进行分配担保,采用“标记-清除”或者“标记-压缩”算法进行回收。

新生代分为Eden区和和两个Survivor区(S1和S2),比例分别为8:1:1,这样就可以根据新老生代的特点选择最合适的垃圾回收算法,新生代的GC也称为MinorGC,老年代的GC称为Full GC。

对象在新生代的分配与回收

大部分对象在很短的时间内都会被回收,对象一般分配在Eden区,当Eden区将满时,会发动一次Minor GC。这个时候JVM会将不需要回收的对象标记出来,复制到S1,将剩下的对象回收。移动到S1的对象年龄+1(年龄即经历的Minor GC的次数)

下一次发动MinorGC时,会将Eden区和S1区不需要回收的对象复制到S2区,对象年龄+1,然后清除Eden和S2的对象。

新生代采用的是复制算法,因为只有极少数对象会存活下来,所以Eden和Survivor的比例是8:1:1.

对象晋升老年代

  • 当对象的年龄到达设定的阈值后,会从Survivor晋升到老年代
  • 大对象分配内存时,会直接在老年代,因为大对象如果分配在Eden复制到Survivor会占用比较大的开销,而且Survivor也容易被占满
  • 在Survivor区相同年龄的对象所占内存之和大于所在区的一半时,则年龄大于或等于该年龄的所有对象晋升到老年代

空间分配担保

在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行Minor GC,如果小于或者不允许冒险则进行一次Full GC。

老年代的GC

一般 Full GC 会导致工作线程停顿时间过长(因为Full GC会清理整个堆中的不可用对象,一般要花较长的时间),如果在此 server 收到了很多请求,则会被拒绝服务!

Minor GC 用的是复制算法,而在老生代由于对象比较多,占用的空间较大,使用复制算法会有较大开销(复制算法在对象存活率较高时要进行多次复制操作,同时浪费一半空间)所以根据老生代特点,在老年代进行的 GC 一般采用的是标记整理法来进行回收。