垃圾收集算法
3.1 概述
- 垃圾收集器(GC)是比Java语言更早的出现。
- 对于Java虚拟机运行时数据区的程序计数器、虚拟机栈和本地方法栈是线程私有的,也就是说是随着线程的创建而创建,随线程的销毁而销毁,所以它们属于自动垃圾回收。对于Java堆和方法区(非堆,老年代)是线程共享的,随着虚拟机的启动而创建,所以需要进行垃圾回收。
3.2 对象的死亡
- 判断对象是否还活着可以使用引用计数算法和可达性分析算法。
3.2.1 引用计数算法
- 原理:每创建一个对象,就给这个对象关联一个计数器,如果对象被引用一次,计数器就加1,对象引用失效,计数器就减1,当计数器的值为0时,就代表当前对象是不可用的。(注意此时对象并不是死了,还需要两次标记过程)
- 缺点:如果两个对象循环引用,那么此种方法就失效了。
3.2.2 可达性分析算法
- 原理:从一个称为GC Roots的对象作为起始点,然后向下进行搜索,搜索经过的路径叫做引用链,当一个对象到GC Roots没有任何引用链相连,则此对象是不可用的。(注意此时对象并不是死了,还需要两次标记过程)
- 可以作为GC Roots对象:
- 虚拟机栈(局部变量表)reference类型引用的对象。
- 方法区中类变量应用的对象(static String str = new String())
- 方法区中常量引用的对象(String str = new String(“123”))
- 本地方法栈JNI(Native方法)引用的对象
3.2.3 四种引用
- 传统的引用定义:对于reference类型的值代表的是一块内存的起始地址,那么这块内存就代表一个引用。
- JDK1.2之后引入了四种引用:强引用、软引用、弱引用、虚引用。
- 强引用:在Java程序中最普遍,形如“Object obj = new Object()”这种就是强引用,只要是强引用还在,垃圾收集器就不会对其回收。
- 软引用:软引用是用来描述一些有用但非必需的对象。对于软引用关联着的对象,在内存溢出将要发生之前,对此类对象进行第二次垃圾回收。如果此时内存还是不够,才会抛出内存溢出异常。通过SoftReference类来实现。
- 弱引用:弱引用也是用来描述一些非必需的对象。对于弱引用关联着的对象,在第二次垃圾回收的时候对其进行回收,不管内存够不够(会不会发生内存溢出)。通过WeakReference类来实现。
- 虚引用:又叫做幽灵引用或者是幻影引用。它是和对象关联最弱的一种引用,并不能通过此类引用创建对象实例,唯一的作用是标记对象在对象被回收的时候收到一个系统通知。可以通过PhantomRefenrence类来实现。
3.2.4 对象死亡的两次标记过程
- 之前的引用计数算法和可达性分析算法判断对象是否还可用,要判断对象是否死亡还需要经过两次标记过程。
- 第一次标记:
- 如果对象对象在可达性分析之后没有到达GC Roots的引用链,那么将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法或者是虚拟机的finalize方法已经执行,那么对象将没有必要执行finalize方法。对象死亡。
- 第二次标记:
- 如果判定对象有必要执行finalize方法,那么这个对象会放置在一个F-Queue队列中,并在稍后虚拟机自动创建的、低优先级的Finalizer线程去执行它。finalize方法是对象逃脱标记的唯一机会,稍后GC 将对F-Queue队列中的对象进行二次小规模的标记,如果此时对象想拯救自己——只需要重新与引用链中的任何一个对象建立关联即可。比如把自己(this)赋值给类变量或者对象的成员变量。此时将会移除标记队列。对象成功逃脱。否则对象死亡。
- 注意:
- 一个对象的finalize方法只会执行一次
- 通过筛选F-Queue队列中的对象,让对象进行自我拯救的方法不鼓励。
3.2.5 回收方法区
- 方法区又称之为永久代,在Java虚拟机规范中说过可以不要求虚拟机在方法区实现垃圾收集,并且在方法区进行垃圾收集的性价比比较低,以为对方法区的垃圾收集主要是针对废弃的常量和无用的类。对堆的垃圾收集一般可以回收70-95%的空间,但是对方法区的效率很低。
- 废弃常量的回收:
- 判断方法:回收废弃常量与回收Java堆中的对象非常相似。
- 当前系统中没有一个对象引用了常量池中的常量。
- 判断方法:回收废弃常量与回收Java堆中的对象非常相似。
- 无用的类的回收:
- 判断方法:
- 1、该类的所有实例对象都已经回收,在Java堆中此时不存在任何该类的对象实例
- 2、加载该类的ClassLoader已经被回收
- 3、该类对应的java.lang.Class对象没有任何在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 在JVM中,对象没用了就一定被回收,满足上面三个条件的类说明是无用的类,但是只是说明该类是可以被回收的。另外还可用通过参数配置来决定要不要对该类进行回收。
- 类回收的相关参数:
- -Xnoclassgc:控制要不要对类回收
- -verbose:class以及-XX:TraceClassLoading:可以在product版虚拟机中查看类加载的消息
- -verbose:class以及-XX:TraceClassUnLoading:可以在FastDebug版的虚拟机中查看类卸载的消息
- 判断方法:
- 虚拟机在何时需要具备类卸载功能?
- 在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,来保证方法区(永久代)不会溢出。
3.3 垃圾收集算法
- 在虚拟机中有多种不同的垃圾收集算法:标记-清除算法、复制算法、标记-整理算法、分代收集算法
3.3.1 标记-清除算法
- 原理:
- 该算法分为“标记”和“清除”两个过程,标记的过程就是判断对象是否无用的两次标记过程。清除的过程就是释放无用对象占用的内存。
- 缺点:
- 1、效率问题,标记和清除的效率都不高
- 2、空间问题,标记完直接清除会产生很多的内存碎片,也就是很多的内存不连续空间,如果接下来在程序运行期间产生大的对象内存分配的时候,出现找不到足够大的内存空间,这样会触发二次垃圾回收。
3.3.2 复制算法
- 原理:
- 复制算法会把内存分成两块等大小的区域,每次使用其中的一块区域,当这一块区域内存使用完了,就会把可用的对象复制到另一块内存,然后一次性清理掉这一块内存,解决了标记-清除算法的内存碎片问题。
- 缺点:
- 将内存一分为二,产生了内存缩小的问题。
- 应用:
- 复制算法主要是当前商业虚拟机针对回收新生代。
- 因为新生代的98%都是属于朝生夕死,此时不需要对内存按照1:1的比例划分,而是将内存划分为Eden空间和From Survivor和To Survivor空间,每次只使用Eden空间和其中的一块Survivor空间,当回收的时候,由于大部分的新生代对象都无用了,所有将存活的对象复制到另一块的Survivor空间。然后清理掉Eden空间和刚才使用的Survivor空间。如果Survivor空间不够用,会依赖其他内存(老年代)进行分配担保。此时这些存活的对象会直接进入到老年代。
- 在HotSpot虚拟机中三者的比例大小为8:1:1。
- 缺点:
- 如果对象的存活率高,需要大批的对象需要复制,不适合。
- 如果对象的存活率高,需要大批的对象需要复制,不适合。
3.3.3 标记-整理算法
- 原理:
- 标记的过程和标记-清除算法的标记过程一样,然后会把存活的对象依次依次向一端移动,最后清理掉端外的内存。
- 标记的过程和标记-清除算法的标记过程一样,然后会把存活的对象依次依次向一端移动,最后清理掉端外的内存。
3.3.4 分代收集算法
- 原理:
- 分代收集算法不是一种新的算法,它只是把Java堆中的对象分成为新生代和老年代。针对新生代复制算法进行回收。针对老年代使用标记-清除或标记-整理算法。
3.4 HotSpot的算法实现
这里是选用了HotSpot虚拟机实现垃圾回收算法。
3.4.1 枚举根节点
- 为什么要枚举根节点?
- 垃圾回收会有标记过程,第一次标记过程会通过可达性分析判断对象是否有到GC Roots的引用链,所以首先要堆GC Roots根节点进行枚举。
- 小注(GC Roots的类型)
- 全局性引用(常量和类静态变量引用的对象)
- 执行上下文(栈帧中局部变量表中的reference类型引用的对象)
- 本地JNI引用的对象
- 枚举根节点为什么要停顿?
- 可达性分析执行对时间的敏感性体现在GC停顿上,因为可达性分析工作需要在一个能保证一致性(见下)的快照中进行,此时GC会停止所有的Java线程。
- 一致性:整个可达性分析期间的整个系统就好像是停顿在某个时间点,不可以出现分析过程中对象引用关系还在发生变化的情况,所以导致了GC会停顿所有的Java线程。
- 枚举根节点时间很长?
- 当前主流的JVM都是使用的准确式GC,所以系统停顿下来后,不需要检查所有的引用,虚拟机是可以知道哪些地方存储着引用。
- 在HotSpot虚拟机中通过OopMap的数据结构实现的,在类加载完成之后,会把每个对象多少偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定位置(安全点)记录下栈和寄存器中哪些位置是引用。GC扫描时就可以直接知道哪里是引用。
3.4.2 安全点
- 枚举根节点给每条指令使用OopMap结构会增加GC的内存空间?
- HotSpot虚拟机并没有给每条指令都生成一个OopMap,只是在安全点才会生成。
- 安全点(Safe point):
- 定义:程序执行过程中,遇到这些点才会停下来执行GC过程。
- 安全点的选取规则:能让程序长时间执行的地方——方法调用、循环跳转和异常跳转。
- 安全点如何实现停下来所有的线程?
- 抢先式中断:不需要线程代码主动去配合,当GC发生时,让所有的线程中断,没有到达安全点的线程就让恢复该线程并执行到安全点。
- 主动式中断:当GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志和安全点时重合的。
- 过程:首先是设置一个标志位,线程执行到标志位处,如果标志位为真,那么线程会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程等待,这样就会触发线程中断。
3.4.3 安全区域
- 为什么会引入安全区域?
- 安全点使得程序在执行过程中保证了进入GC过程,但是如果是程序在不执行的时候,会通过设置安全区域进入GC过程。
- 程序不执行:就是处理器没有给程序分配CPU时间,比如线程进入sleep和blocked状态
- 安全区域:
- 定义:在一段代码中,引用关系不发生变化。在这个区域的任何一个地方都是安全的。
- 在执行到安全区域的代码,首先标识了自己进入了安全区域,当在这段时间里,如果JVM发起GC,就不用管标识自己为safe region状态的线程了,在线程离开safe region时,会检查系统是否已经完成了根节点枚举,如果完成了线程就继续执行,如果没有完成就必须等待直到收到可以安全离开safe region的信号为止。