第三章,垃圾收集器
概述
一、概述
1.垃圾收集器(GC)需要思考的三件事情:
哪些内存需要回收?
什么时候回收?
如何回收?
2.Java内存运行数据区域中,程序计数器、虚拟机栈、本地方法栈、三个区域跟线程的声明周期相同。方法结束或者线程结束时,内存就跟着回收了。而Java堆和方法区不同,我们只有程序处于运行期间才会知道创建那些对象,这部分内存的分配和回收都是动态的。垃圾收集器所关注的就是这部分。
二、如何判断对象已经死亡
垃圾收集器在回收对象之前,第一步就是要判断对象是否应该回收。
1.引用计数算法:
(1)给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器的值就减1;任何时刻计数器为0的对象不可能在被使用。
(2)优点:实现简单,判定效率高。
(3)缺点:很难解决对象之间相互循环引用的问题。
2.可达性分析算法:
(1)通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径称之为引用链,当一个对象到GC Roots没有任何引用时,就是GC Roots到这个对象不可达,证明对象是不可用的。
(2)GC Roots对象包括以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
(3)引用:JDK1.2之后,对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种。
强引用就是指在程序代码之中普遍存在的,只要强引用还在,GC永远都不会回收掉被引用的对象。
类似于“Object obj = new Object()”这类的引用。
软引用是用来描述一些有用但并非必须的对象。在系统将要发生内存溢出异常时,才会回收软引用。
如何回收完后仍然没有足够的内存,则会抛出内存溢出异常。
SoftReference类来实现软引用。
弱引用是用来描述非必须对象的。关联的对象可以生存到下一次GC发生之前。只要GC工作,就会回收弱引用。
WeakReference类来实现弱引用。
虚引用成为幽灵引用或者幻影引用,最弱的一种引用关系。
PhantomReference类来实现虚引用。
(4)在可达性分析算法中,不可达对象判断为不可达到真正被回收至少要经过两次标记过程。第一次为经过分析没有和GC Roots引用链相连,会被第一次标记并且进行一次筛选。判断对象是否需要执行finalize()方法。
如果对象没有覆盖finalize方法,或者说finalize方法已经调用过,虚拟机将这两种情况都视为“没有必要执行。”
任何一个对象的finalize方法只会调用一次。
(5)回收方法区。方法区主要回收两部分内容,包括废弃常量和无用的类。
回收废弃常量和回收Java堆的对象非常相似,如何常量没有任何引用。则被认为是废弃常量。
其他类(接口)、方法、字段的符号引用也与此类似。
废弃的类需要满足以下三个条件:
该类的所有实例都已经被回收,Java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有在任何地方被引用,在任何地方都无法通过反射访问该类的方法。
三、垃圾收集算法
1.标记 – 清除 算法
(1)思想:首先标记出所有要清理的对象,在标记完成之后他统一回收所有被标记的对象。
(2)不足:
效率问题:标记和清除两个过程的效率都不高。
空间问题:标记清除算法容易产生不连续的内存碎片。
PS:内存碎片太多将会导致在分配大对象的时候无法找到连续的内存空间而不得不提前触发另一次垃圾收集操作。
2.复制算法
(1)解决 标记 – 清除 算法的效率问题。
(2)适用场景:新生代GC,新生代特点,对象存活时间短,存活率低,回收率高。
(3)思想:将内存按照划分成大小相等的两块,每次将活着的对象复制到另一块内存中。然后将另一半清理掉。
(4)真实内存划分:根据IBM公司研究表明,98%的对象都是朝生夕死的,所以并不需要1:1分配内存空间。只需要将内存空间分为一块较大的空间Eden区以及两块较小的空间Survivor区。HotSpot虚拟机默认Eden区和Survivor区的大小比例为8:1,也就是说每次使用新生代80%的空间。
(5)复制算法时,内存不足:如果在将Eden和一个Survivor区中的对象复制到另一半Survivor区,而另一半Survivor内存不足时,将会通过分配担保进入其他内存(这里指老年代)
3.标记 – 整理算法
(1)解决 标记 – 清除 算法的生成内存碎片的问题。
(2)适用场景:老年代GC,老年代特点,对象存活率较高,没有额外空间。
(3)思想:将内存中的对象标记,然后将存活的对象向一端移动,直接清理掉端边界以外的内存。
4.现代商业虚拟机大都采用“分代收集算法”,指的是根据对象存活周期不同,把内存划分为几个区域,一般都为新生代和老年代。根据不同区域的特性来配置响应的收集算法。
四、HotSpot 的算法实现
1.枚举根节点:可达性分析,这项工作需要在保证一致性的快照中进行。枚举根节点需要停顿。
2.准确式GC:虚拟机可以知道某个位置记录了什么数据。在HotSpot实现中,使用一组称为OopMap的数据结构来达到这个目的。
3.安全点:在OopMap的协助写,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题是:OopMap内容变化指令非常多,为每一条指令都生成一个OopMap将会需要大量的额外空间,这样GC的空间成本非常高。实际上,OopMap只在特定的位置记录了这些信息,这些位置称为“安全点”,即程序运行到这里不会导致对象的引用关系变化。
4.如何在GC发生时让线程都跑到安全点停顿下来。有两种方法,分别是抢断式中断和主动式中断,抢断式中断指的是,GC发生时把所有线程全部中断,发现没有到安全点的线程恢复起来跑到安全点。主动式中断指的是设置一个标志,让所有线程去轮询这个标志,轮询标志和安全点是重合的,判断在轮询标志中的线程就自己中断挂起。当前HotSpot使用的是主动式中断。
5.安全区域指的是对象的引用不会发生变化的区域。
五、垃圾收集器
七种垃圾收集器的特性、基本原理和使用场景。
1、Serial收集器:Serial收集器是最基本、发展历史最悠久的收集器。这是一个单线程收集器。它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,而且在垃圾收集过程中,虚拟机中其他线程必须暂停,等待该线程收集完成。
优点:简单而高效
缺点:需要停顿
算法:新生代采用复制算法,老年代采用 标记-整理 算法。
使用场景:虚拟机Client模式下默认的新生代收集器。
2、parNew收集器:ParNew是Serial的多线程版本。除了使用多线程收集外,其他方面与Serial没有任何区别。
优点:简单而高效
缺点:需要停顿
算法:新生代采用复制算法,老年代采用 标记-整理 算法。
使用场景:虚拟机Server模式下默认的新生代收集器。唯一可以配合CMS收集器工作的垃圾收集器。
3、Parallel Scavenge收集器:多线程处理器,吞吐量优先。吞吐量指的是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码的时间 + 垃圾收集时间 ) 。提供 -XX:MaxGCPauseMilis 用来控制最大垃圾收集停顿时间。 提供 -XX:GCTimeRatio 用来控制 GC时间占总时间的比率。提供 -XX:UseAdaptiveSizePolicy用来进行GC自适应调节策略。
优点:能够控制垃圾收集时间的吞吐量。
缺点:效率相对较低。
算法:新生代采用复制算法,老年代采用 标记-整理 算法。
使用场景:适合后台运算而不需要太多交互的任务。
4、Serial Old收集器:是Serial的老年代版本,单线程收集器,使用 标记-整理 算法。可作为CMS收集器的后备预案,在并发手机发生Concurrent Mode Failure时使用。
5、Parallel Old收集器:Parallel Old是Parallel Scavenge收集器的老年代版本,多线程收集器,使用 标记-整理 算法。吞吐量优先原则。
6、CMS收集器:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用 标记-清除 算法。
整个过程需要4个步骤:
初始标记:需要“Stop The World”,标记一下GC Roots能直接关联到的对象,速度很快。
并发标记:是进行GC Roots Tracing的过程。
重新标记:需要“Stop The World”,修正并发标记期间,用户程序继续运行而导致标记产生变动的那一部分标记。停顿时间稍长,但比初始标记短。
标记清除:和用户线程并发的收集垃圾清理工作。
由于整个收集过程中耗时最长的两个部分,并发标记以及标记清除都可以和用户线程一起运行,所以CMS收集器可以看作是和用户线程一起运行的。
优点:并发收集、低停顿。
缺点:
1、对CPU资源非常敏感。默认回收线程数是(CPU数量 + 3)/ 4 。CPU数量越多,CMS消耗资源相对来说越少。
2、无法处理浮动垃圾。可能出现“ConCurrent Mode Failure”失败而导致另一次Full GC的产生。
3、标记-清除算法会产生空间碎片。
7、G1收集器:面向服务器端应用的垃圾处理器。整体来看是采用 标记 – 整理 算法,而局部上来看是基于 复制 算法来实现的。
G1与其他垃圾收集器相比具备如下特点:
1、并发与并行:能使用多个CPU来缩短停顿的时间,多线程收集。
2、分代收集:G1收集器存在分代概念,但是单独管理整个堆。
3、空间整合:整体基于标记-整理算法,局部基于复制算法。都不会产生空间碎片。
4、可预测的停顿:在缩短停顿时间的基础上,能够建立停顿时间模型。
G1收集器将整个堆划分成多个相同大小的独立区域(Region),并维护一个优先列表,用于跟踪记录每一个Region的回收价值(根据回收获得的空间大小以及回收所需的时间确定),每次在允许的时间内,优先回收价值最大的Region(这就是Garbage-First名称的由来)。
虚拟机为了防止G1收集器扫描全部Region或者其他收集器扫描新生代以及老年代。G1中Region的对象引用以及其他收集器新生代与老年代之间的对象引用,虚拟机使用Remembered Set(RSet)来避免进行全堆扫描。
G1运行大致可以分为:
1、初始标记:标记一下GC Roots能直接关联到的对象。
2、并发标记:从GC Roots开始进行可达性分析。
3、最终标记:修正并发标记期间用户程序运行导致的标记变动。
4、筛选回收:进行Region的回收价值排序并进行垃圾回收。