JVM-垃圾回收算法
Published in:2023-03-01 | category: 学习

判断对象已死:

1、引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一,当引用失效时,计数器就减一。任何时刻计数器为零的对象就是不可能再被使用的。

优缺点:

占用了一些额外的内存空间来计数,但它的原理简单,判定效率也高。

主流的Java虚拟机没有选用引用计数法来管理内存。主要原因时这个算法有很多例外情况要考虑,必须配合大量的额外处理才能保证正确的工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

2、可达性分析算法

通过一系列称为”GC Roots“的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为”引用链“。如果这个对象多GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象时不能再被使用的。

可以作为GC Roots的对象包括以下几种:

1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆中使用到的参数、局部变量、临时变量等。

2、在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

3、在方法区中常量引用的对象,譬如字符串常量池里的引用。

4、在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

5、Java虚拟机内部的引用,如基本数据类型对饮的Class对象,一些常驻的异常对象等,还有系统类加载器。

6、所有被同步锁(synchronized关键字)持有的对象。

7、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

被可达性分析算法判定为不可达时一定”非死不可“吗?

不是”非死不可“。要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么它将会被第一次标记。随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为”没有必要执行“。

如果这个对象被判定为确有必要执行finalize()方法,该对象会被放入一个F-Queue的队列中,然后在一条由一条虚拟机自动创建的,低调度优先级的Finalizer线程去执行它们的finalize()方法。但是”执行“并不承诺一定会等待它运行结束。因为如果某个对象的finalize()方法执行很慢,或者是发生死循环,会导致队列中其他对象处于等待状态,甚至导致整个内存回收子系统崩溃。

finalize()方法是对象拯救自己的最后一次机会,随后收集器会对F-Queue中的对象进行第二次标记。如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联,成功拯救自己,那第二次标记时它就会被移出”即将回收”的集合。

值得注意的是,finalize()方法只能被系统自动调用一次,如果对象面对下一次回收,它的finalize()方法不回被再次执行,不能再次自救。

引用:

1、强引用

是最传统的”引用“的定义,是指在程序代码之中普遍存在的引用赋值,当用new创建对象的时候就是强引用。无论任何情况下,只要强引用的关系还在,垃圾回收器永远不会回收掉被引用的对象。

2、软引用

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进收回范围之中进行二次回收。就是内存不满的时候不会回收,在内存满了之后会进行回收操作。

3、弱引用

弱引用也用来描述那些非必须的对象,但是他的强度比软引用更弱一些。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

4、虚引用

它是最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

垃圾收集算法:

分代收集理论:

当前商用虚拟机的垃圾收集器,大多数遵循了“分代收集”的理论进行设计。他建立在分代假说之上:

1)弱分代假说:绝大多数对象都是朝生夕灭的。

2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

3)跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

收集器应该将Java堆中划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之后存储。

优缺点:同时兼顾了垃圾收集的时间开销和内存空间的有效利用。

标记-清除算法:

算法分为“标记”和“清除”两个阶段:首先标记出所有要被回收的对象,在标记完成后,同意回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记的过程就是对象是否属于垃圾的判定过程。

优缺点:

1、执行效率不稳定:如果Java堆中包含大量对象,而且其中大部分是要被回收的,这是必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量的增长而降低。

2、内存空间碎片化:标记、清除之后回产生大量不连续的内存碎片,空间碎片太多可能会导致当以后再程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发了一次垃圾收集动作。

标记-复制算法:

标记-复制算法常被简称为复制算法。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。

优缺点:

1、如果内存中多数对象是存活的话,这种算法将会产生大量的内存空间复制的开销。

2、对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存的时候也不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。实现简单,运行高效。

3、可用内存缩小为了原来的一半,内存浪费未免太多了。

标记-整理算法:

标记的过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。

优缺点:

1、如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象的移动操作必须全程暂停用户应用程序才能进行,这就让使用者不得不小心翼翼地权衡其弊端,像这种的停顿被最初的虚拟机设计者形象地描述为“Stop The World”。

2、不像标记-复制算法那样浪费额外的空间,也不会像标记-清除算法一样产生空间碎片。

Prev:
杨辉三角(算法)
Next:
JVM-Java内存区域