自己做的网站如何在百度搜到,网站建设代理成本,中国传统美食网页制作素材,it外包网JVM系列 | 垃圾收集算法 文章目录 前言如何判断对象已死#xff1f;引用计数法可达性分析算法可达性分析2.0版 | 引用的增强对象的消亡过程回收方法区主要回收目标#xff1a;回收操作 垃圾收集算法分代收集理论 与 跨代引用假说分代收集理论跨带引用假说 垃圾收…JVM系列 | 垃圾收集算法 文章目录 前言如何判断对象已死引用计数法可达性分析算法可达性分析2.0版 | 引用的增强对象的消亡过程回收方法区主要回收目标回收操作 垃圾收集算法分代收集理论 与 跨代引用假说分代收集理论跨带引用假说 垃圾收集算法 | 标记清除算法垃圾收集算法 | 标记复制算法传统标记复制算法优化标记复制算法逃生门 | 提前养老 垃圾收集算法 | 标记整理算法 前言 在上一篇文章《【JVM】对象的生命周期一 | 对象的创建与存储》中我们已经介绍了对象的完整创建过程既然有出生那么必然有死亡。本文将会详细介绍对象的消亡-JVM的垃圾回收机制。 这两篇文章详细介绍了对象的生命周期推荐联合观看。 如何判断对象已死 引用计数法 JVM 并不是使用引用计数器来判断对象是否存活的这是由于引用计数器有着非常大的缺点。 引用计数法非常的简单在对象中添加一个引用计数器每当有一个地方引用它计数器的值就1当引用失效时计数器-1。当计数器为0时就代表没有地方引用它它就可以被垃圾回收器清除掉。
想法很完美但是如果两个对象互相引用那么即使这两个对象已经不存在其它的调用关系也不会被垃圾收集算法清理掉请看以下代码
class Node {Node next;
}public class ReferenceCountingExample {public static void main(String[] args) {Node node1 new Node();Node node2 new Node();// 互相引用node1.next node2;node2.next node1;// 取消外部引用node1 null;node2 null;// 在引用计数垃圾回收器中node1 和 node2 由于互相引用// 引用计数永远不会变为0它们不会被回收。}
}以上代码中 创建两个node对象每个node对象的引用计数器为1让他们的nextNode指向对方现在互相引用每个node的引用计数器是2清除外部引用也就是让node1/node2变为null此时每个node的引用计数器是1没有地方再引用node1/2了但是node1/2还是无法正常退出 可达性分析算法
可达性分析算法从根对象(GC Roots)出发注意根对象可以不止有一个下面会有介绍一级一级向下扫描能被根对象直接或间接引用的对象就是存活对象从根对象可达与根对象没有任何关系的对象则为消亡的对象根对象不可达。 上图中
O2/O3/O4与根对象O1间接可达那么在本次扫描中该三个对象全部为存活O6与O7对象虽然与O5对象关联但是O5对象并没有与根节点关联因此O5/O6/O7对象全部消亡O8/O9对象虽然互相引用但是也不例外消亡 可达性分析2.0版 | 引用的增强
在Java 1.2之前引用只有传统的实现方式可达即存活、不可达即消亡。
但是在一些场景下有一些对象存在能创造一定的价值但是消亡了意义也不大典型的例子就是缓存。缓存中存在的内容可能是一些大的对象我们通过缓存可以加快程序的运行速度。但是如果缓存的内容太多那么会严重影响JVM的运行速度此时Java都跑不动了还关心数据库读取什么的嘛这个时候就可以释放掉这些引用内容。
为了解决这一问题JDK引入了另外三种引用方式分别如下
强引用经典引用 Strongly Reference软引用Soft Reference弱引用Weak Reference虚引用Phantom Reference 强引用是最传统的“引用”的定义是指在程序代码之中普遍存在的引用赋值即类似“Object objnew Object()”这种引用关系。无论任何情况下只要强引用关系还存在垃圾收集器就永远不会回收掉被引用的对象。 软引用是用来描述一些还有用但非必须的对象。只被软引用关联着的对象在系统将要发生内存溢出异常前会把这些对象列进回收范围之中进行第二次回收如果这次回收还没有足够的内存才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。 弱引用也是用来描述那些非必须对象但是它的强度比软引用更弱一些被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作无论当前内存是否足够都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。 虚引用也称为“幽灵引用”或者“幻影引用”它是最弱的一种引用关系。一个对象是否有虚引用的存在完全不会对其生存时间构成影响也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。 对象的消亡过程
上文中我们已经确定了判断对象的消亡的方法但是并不是一旦发现消亡对象之后就立刻进行清除要清除一个对象至少要经过两次标记阶段。
阶段一判定对象为不可达
阶段二判断对象是否重写了finalize()方法(终结器方法)如有没有重写该方法则直接进行垃圾回收/如果重写了该方法那么将会把该对象放在名为F-Queue队列中并在稍后由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行但并不承诺一定会等待它运行结束以防止执行缓慢或死循环等。
阶段三对F-Queue队列中的对象进行二次标记。此时会查看这些对象是否仍然不可达如果不可达那么这些对象将会被垃圾回收器进行回收。 如过finalize方法中有代码把该对象的引用重新赋值给某个静态变量或其他存活的对象该对象又可达了那么该对象将不会被垃圾回收掉。 可见对象的消亡像是一个判刑的过程如果对象一开始犯了错不可达那么就先要判断该对象有没有必要缓刑重写finalize方法然后JVM将不缓刑的对象直接死刑立即执行将需要缓刑的对象关到一个单独的监牢里面随后给缓刑的对象们一个托关系的机会找到机会了就能活没有机会就得消亡。 回收方法区 简单复习:方法区是用于存储已被虚拟机加载的类信息类名、访问修饰符、父类、接口、字段、方法等、常量、静态变量、即时编译器编译后的代码字段的名称与描述符、方法的字节码、访问修饰符等数据。方法区在JVM规范中是堆的一部分但在实现上可以有不同的划分和管理方式。 对方法区的回收性价比很低在Java堆中对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间方法区则远低于此。因此也有一些垃圾收集器没有实现对方法区的回收。 主要回收目标
废弃的常量不再使用的类型 回收操作 回收常量比较简单如果一个字符串Jim.kk进入到常量池但是又没有任何一个字符串对象值是它那么他就可以被清理出去。 回收不再使用的类型回收不再使用的类型比较麻烦它要同时满足以下条件才能够允许被回收。而且并不一定会被回收需要程序员使用参数控制。 该类所有的实例都已经被回收也就是Java堆中不存在该类及其任何派生子类的实例。加载该类的类加载器已经被回收这个条件除非是经过精心设计的可替换类加载器的场景如OSGi、JSP的重加载等否则通常是很难达成的。该类对应的java.lang.Class对象没有在任何地方被引用无法在任何地方通过反射访问该类的方法。 启用条件:关于是否要对类型进行回收HotSpot虚拟机提供了-Xnoclassgc参数进行控制还可以使用-verboseclass以及-XXTraceClass-Loading、-XXTraceClassUnLoading查看类加载和卸载信息其中-verboseclass和-XXTraceClassLoading可以在 Product版的虚拟机中使用-XXTraceClassUnLoading参数需要FastDebug版[1]的虚拟机支持。 在大量使用反射、动态代理、CGLib等字节码框架动态生成JSP以及OSGi这类频繁自定义类加载器的场景中通常都需要Java虚拟机具备类型卸载的能力以保证不会对方法区造成过大的内存压力。 垃圾收集算法 分代收集理论 与 跨代引用假说
分代收集理论
当我们使用引用可达算法扫描堆内存的时候会发现大部分的对象都是朝生夕死的存活时间可能根本不会超过一次垃圾收集。还有一些对象是长命百岁的能一直活很能活。
对于这种情况垃圾收集器提出了新生代与老年代的概念所有新创建的对象放在新生代中并定时进行扫描与垃圾回收能挺过多次垃圾回收的对象将被放入老年代中并在老年代中以更低的频率来回收该区域。这就同事兼顾了垃圾收集的时间开销和内存的空间利用。 跨带引用假说
对象与对象之间可能存在跨代引用比如老年代引用新生代的对象但是新生代的大部分都是朝生夕死的所以跨代引用必定是小数如果广泛存在的话则大部分新生代对象肯定都能存活很久没有必要为了这一小部分跨带引用去扫描整个老年代因此JVM建立了一个称为记忆集的数据结构存储在新生代中用来记录老年代的哪一块老年代的内存存在跨带引用这样的话在扫描新生代时只需要扫描一下这些被记录的老年代即可。 上图中在第二个与第五个老年代的分片上存在跨代引用分别是o7引用Y11/o20引用Y25将这两个区域记录在记忆集中随后在对新生代进行垃圾收集的时候从记忆集中拿到两个老年代的内存区域并进行扫描所以最终扫描对象除了所有新生代的对象以外还包含(o5、o6、o7、o8、o17、o18、o19、o20)。 事实上并不只是跨老年代与新生代之间才存在跨代引用与记忆集许多的分代或者分区的垃圾收集器中都存在跨代引用比如近些年很火的G1收集器没有明确的新生代与老年代整个堆内存就是无数的小分区。 垃圾收集算法 | 标记清除算法
标记清除算法是最早出现的算法在1960年有Lisp之父John McCarthy提出当时还没有Java语言不止是只有Java才有虚拟机与垃圾回收。
标记清除算法分为两个步骤1. 标记 2. 清除。可以对所有需要回收的对象做标记随后统一清除掉也可以对不需要回收的对象做标记随后统一清除掉没有标记的对象。 标记回收算法有两个缺点
是执行效率不稳定如果Java堆中包含大量对象而且其中大部分是需要被回收的这时必须进行大量标记和清除的动作导致标记和清除两个过程的执行效率都随对象数量增长而降低内存空间的碎片化问题标记、清除之后会产生大量不连续的内存碎片空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 在之前一篇文章《【JVM】对象的生命周期一 | 对象的创建与存储》中提到过使用空闲列表来记录堆内存中的空闲内存随后在空闲内存中插入新对象的方式。这种方式就适用于标记清除算法在一段时间使用后堆内存中会存在大量的空隙造成很严重的内存浪费。 垃圾收集算法 | 标记复制算法
传统标记复制算法
标记复制算法又称为标记移动算法它解决了标志清除算法中大量内存空间碎片的问题。
简单来说标记复制算法就是将需要进行垃圾回收的区域分为两个部分可以是新生代也可以是老年代在创建新的对象的时候只使用其中的一半在需要进行垃圾回收的时候先对对象进行标记然后将能存活的对象复制到另一半当前区域的对象全部消亡注意当前区域与目标区域不是新生代与老年代的区别。 标记复制算法也有一个非常大的缺点内存空间浪费标记复制算法总有一半的内存空间是未被使用的。 优化标记复制算法
为了解决标记复制算法带来的巨大空间浪费问题Andrew Appel针对具备“朝生夕灭”特点的对象提出了一种更优化的半区复制分代策略现在称为“Appel式回收”。
Appel不是Apple哦式回收采用一个大的Eden空间两个小的Survivor空间Eden:Survivor通常为8:1)新建对象时将对象存放至Eden空间进行垃圾回收时扫描Eden空间与其中一块Survivor空间并将存活的对象全部移动至另一块Survivor空间。 上一次垃圾回收存活的对象放在Survivor1中本次垃圾回收扫描Eden空间与Survivor1空间并将所有存活对象放入另一个Survivor2空间中以此循环。 逃生门 | 提前养老
虽然朝生夕死大部分情况下可以消灭98%的对象但是毕竟也会有特殊情况万一那10%的Survivor无法存储本次GC可以存活下来的对象怎么办呢Appel式垃圾回收提出了逃生门机制
在通常情况下对象进入老年代存在一个阈值比如一个对象连续存活超过20次可以进入老年代。但是当触发逃生门机制的时候Survivor无法存放全部存活对象就会让一部分存活了一段时间但是还未达到阈值的对象提前进入老年代这样可以保证Survivor空间的正常。
垃圾收集算法 | 标记整理算法
标记整理算法在需要GC时会先标记所有对象然后将不需要存活的对象从内容空间中剔除给存活的对象整齐的复制到内存的开端。 标记整理算法相比于标记移动算法节省了内存空间但是移动所有的存活对象并更新所有引用是一件及其负重的操作而且在这一阶段之内用户线程无法继续执行否则可能会造成空引用等问题因此这样的停顿被最初的虚拟机设计者描述为“Stop The World”有点类似于时停的意思。