GC¶
标记阶段¶
引用计数算法¶
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。
但是,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
可达性分析算法¶
- 通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索。
- 搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连, 即从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
固定可作为GC Roots的对象包括以下几种:¶
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI (即通常所说的Native方法) 引用的对象。Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
由于 root 采用栈的方式存放变量和指针, 所以一个指针保存了堆内存的对象,但自己不存放在堆内存,则它是 root。
finalize()¶
一个对象死亡,至少要经历两次标记过程
- 如果对象在进行可达性分析后发现没 有与GC Roots相连接的引用链,那它将会被第一次标记
- 随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
- 假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
- 如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的 队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finaliz er线程去执行它们的finalize() 方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。 这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导 致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可。
虚拟机中对象的三中状态:
- 可触及的: 从根节点开始,可以到达这个对象。
- 可复活的: 对象的所有引用都被释放,但是对象有可能在finalize()中复活。
- 不可触及的: 对象的finalize() 被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize() 只会被调用一次。
清除阶段¶
标记-清除算法¶
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记: Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
- 清除: Collector对 堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点:
- 执行效率不稳定,如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低;
- 内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作
- STW
清除:把需要清除的对象保存在空闲的地址列表。
标记-复制算法¶
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费大
应用:需要存活对象少、垃圾对象多的情况
新生代,eden, s0, s1。
标记-压缩算法¶
标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
标记-整理 是在 标记-清除 之上,又进行了 对象的移动排序整理,因此 成本更高,但解决了 内存碎片 的问题
优点:只需一个起始内存地址; 无需内存减半
缺点: 效率低于复制算法; 若移动的对象被引用, 还需修改引用地址; STW
分代收集¶
收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区 域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那 么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对 象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
增量收集算法¶
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:由于线程切换等原因,导致吞吐量下降
分区算法(G1)¶
在相同条件下,堆空间越大,一次 Gc 时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。