1. 概述
说起垃圾回收器(Garbage Collection GC),许多人把它当做Java的产物了;其实在Java出现以前就有使用内存动态分配和垃圾回收技术的语言了(Lisp)。
垃圾回收要考虑三件事情:
- 哪些内存需要回收
- 什么时候回收
- 怎么样回收
Java运行时内存分为多个部分,程序计数器、堆、栈、方法区和本地方法栈。其中程序计数器、本地方法栈和栈都是和线程的生命周期相同,随线程生,随线程灭。每个栈帧分配多少内存在类结构确定时就已知了,这几个内存的分配和回收具有确定性,不需要过多考虑内存回收,因为方法结束或线程结束,内存自然随着回收了。而Java堆和方法区不一样,一个接口的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也不一样,只有在运行时才知道实例化的是哪个类,这部分内存的分配和回收都是动态的,垃圾回收器主要关注这部分内存。
2. 对象存活
Java堆里存放了几乎所有的对象,垃圾回收器回收内存之前,首先要做的就是判断哪些对象活着,哪些对象已经死了。
2.1 引用计数算法
给对象一个引用计数器,有一个地方引用它,计数器加1,引用失效,计数器减1,计数器为0的对象不可能再被使用。
缺点:没办法解决对象循环引用问题。
2.2 可达性分析
这个算法就是通过一系列的“GC Root”为起始点,从这些起始点向下搜索,搜索经过的路径称为引用链,当一个对象到GC Root没有引用链的时候,这个对象称为不可达对象,则证明这个对象不可用,判断为可回收对象。
Java语言中可作为GC Root的对象有:
- 虚拟机栈帧中变量表所引用的对象
- 方法区中静态属性所引用的对象
- 方法区中常量所引用的对象
- 本地方法栈中Native方法引用的对象
2.3 引用
一块内存如果存放的是另一块内存的起始地址,则称这这块内存是一个引用。这种定义,一个对象只有被引用和未被引用两种状态。Java对引用定义做了扩展,将引用从强到弱一次分为强引用(Strong Refenrence)、软引用(Soft Refenrence)、弱引用(Weak Refenrence)、虚引用(Phantom Refenrence)。
- 强引用在代码中普遍存在,类似于“A a = new A();”这类引用,只要强引用存在对象就不会被垃圾回收器回收。
- 软引用用来描述一下有用但不是必须的对象。对于软引用关联的对象,在发生内存溢出异常之前,讲这些对象列入内存回收范围之中进行第二次回收。用SoftRefenrence类实现软引用。
- 弱引用也是用来描述非必须的对象,但是比软引用更弱,只能生存到下次内存回收之前,不管内存是否够用,都会被回收。用WeakReference实现。
- 虚引用是最弱的一种引用关系,又称为幽灵引用或幻影引用。虚引用的存在不对对象的生存周期造成影响,也无法通过虚引用获取对象实例。存在的唯一目的是为了在对象被回收的时候得到系统通知。用PhantomRefenrence实现。
2.4 Finalize方法
即使对象被判定为不可达,也并非会立即被回收,先进行标记,并进行筛选判断是否有必要执行finalize()方法,当对象没有被覆盖finalize()方法,或者finalize()已经被虚拟机调用过,则没必要执行。如果被判断有必要执行,将对象加入F-Queue队列,并由虚拟机创建一个低级别的Finalizer线程去执行(如果执行finalize()发生死循环,队列中其他对象将永久等待,可能导致整个回收系统崩溃)。finalize()是对象逃脱死亡的最后机会,只要能和引用链上的任何对象建立关系,比如把this赋给某个类变量或对象的成员变量,这样在第二次标记的时候就会把对象移除待回收的集合。如果没有逃脱,那就真的被回收了。
2.5 方法区回收
方法区回收性价比较低,主要分为两部分:废弃的常量和无用的类。
以常量池中的字符变量为例,假如“abc”进入了常量池,但是没有任何String变量引用,如果这时发生内存回收,而且有必要的话,“abc”会被清理出常量池。常量池中的其他类(接口)、方法和字段的符号引类似。
无用的类判断则比较苛刻:
- 该类的所有实例都已经被回收
- 加载该类的classloader已经被回收
- 该类的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3. 垃圾回收算法
3.1 标记清除
首先标记处所有需要回收的对象,标记完成后,统一回收被标记的对象。缺点是标记和回收的效率都不高,而且产生大量不连续的内存碎片。
3.2 复制算法
为了解决效率问题,复制算法出现了。复制算法将内存平均分为两块,每次分配只使用一块,当内存不够时,将还活着的对象复制到另一块内存,然后将使用过的内存清理掉。这样分配不需要考虑内存碎片,但是可用内存缩小为原来的一半,代价有点太大。
3.3 标记整理
标记整理和标记清除一样,但后续不是直接清理不可用的对象,而是把可用对象向内存一端移动对齐,然后直接清理掉端边界以外的内存。
3.4 分代收集
分代收集没有什么新的算法思想,只是根据对象的生存周期不同将内存划分成几块。一般把Java堆分为新生代和老年代,这样就可以根据各年代的特点选择合适的收集算法。新生代每次收集都有大量对象死去,少量存活,就可以选用复制算法。而老年代的对象存活率高、没有额外的空间,就可以使用标记清理或标记整理。
4. Hotspot算法实现
4.1 枚举根结点
以可达性分析从GC Root节点找引用链为例,可作为GC Root节点的引用在全局引用(常量或类的静态变量)和执行上下文中(栈帧中的本地变量表)。
GC分析对时间敏感,分析工作必须在一个能确保一致性的快照中执行 —— “一致性”指的是,在整个分析期间执行系统看起来像被冻结在某个时间,不可以出现分析期间对象引用还在发生变化的情况。这点导致GC进行时必须停止所有线程(Stop The World)。
现在系统方法区能达到几百兆,遍历引用是很耗时的。主流的Java虚拟机都采用准确式GC,系统停顿时不需要一个不漏的检查完执行上下文和全局的引用位置。Hotspot的实现中,使用一组叫OopMap的结构达到这个目的。类加载完成的时候,hotspot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译中,也会在特定位置记录下栈和寄存器中哪些位置存放的是引用。这样,在GC扫描的时候即可以获取这些信息。
4.2 安全点
在OopMap的协助下,可以很快的完成GC Root的枚举,但是导致引用发生变化(OopMap内容变化)的指令太多了,如果每一条都记录,需要大量的额外空间,GC成本太高。
实际上,Hotspot没有每条指令都生成OopMap,只在“特定位置”记录这些信息,这个位置称为安全点(safepoint),也就是说程序执行不是在所有位置都能停下来GC,只有到达安全点才能停顿。安全点的选择是以“是否具有让程序长时间执行的特征”为标准的,例如方法调用、循环跳转
异常跳转等。
对于safepoint另一个问题是如何让所有线程都跑到最近的安全点停下来。有两种方案:抢断式中断和主动式中断。
抢断式中断:发生GC时,先把所有线程都中断,如果发现有中断不在安全点的线程,就恢复线程,让其跑到最近的安全点上。现在几乎没有虚拟机用这种方式。
主动式中断:其思想是需要发生中断时,不直接对线程操作,只是设置一个全局的中断标识,各线程主动轮训访问这个标识,发现中断标识为真,就主动中断线程。轮训标志的位置和安全点是重合的。
4.3 安全区
safepoint似乎完美解决如何进入GC的问题,执行时不太长时间就会进入saftpoint,但是如果程序不执行呢,比如线程处于block或sleep状态。这种情况需要安全区域(safe region)来解决。
安全区域是指,在一段代码中,医用关系不会发生变化,在这个区域中任何位置执行GC都是安全的。
在线程进入safe region时,首先标识自己进入安全区域,这样期间发生GC时,就不管进入安全区域的线程了。在离开安全区域时,首先检查系统是否完成根结点的枚举,如果已经完成就继续执行,否则就要等待直到收到可以安全离开safe region的信号为止。
5. 垃圾收集器
如果说收集算法是内存回收算法的方法论,那么垃圾收集器就是内存回收的具体实现。
5.1 serial收集器
serial是最基本的历史最悠久的垃圾收集器,它是单线程的,单线程的意义不只是只有一个线程完成垃圾回收工作,更重要的是,在进行垃圾回收的时候,必须暂停其他所有工作线程,知道收集完成。
缺点很明显,优点是简单高效(与其他单线程收集器相比),对于单核机器来说很合适,没有线程交互开销。
5.2 ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾回收之外,其他和serial收集器没有区别。
5.3 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以最短停顿时间为目标的收集器。集中应用在互联网站或B/S系统的是服务器,这类系统尤其重视响应速度,希望停顿时间最短,给用户带来好的体验。CMS收集器使用标记-清除算法。
收集过程分为四步:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
缺点:CPU资源敏感、浮动垃圾、空间碎片