1. 概述
(1)垃圾收集(Garbage Collection,GC)需要完成三件事情:
• 哪些内存需要回收
• 什么时候回收
• 如何回收
(2)为什么要去了解垃圾收集:
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时。
(3)垃圾收集区域:
由于程序计数器、虚拟机栈、本地方法栈是线程私有的,这意味着这三个区域是随线程生、随线程死,线程结束时,内存就随着回收了,所以这三个区域内存不需要额外花费很多心思去考虑如何回收。
而Java堆和方法区是线程共享的,其内存分配具有动态性,回收时机是不确定的,所以垃圾收集针对的就是这两个区域。
2. 判断对象是否应该回收
2.1 引用计数算法
原理:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器值为零的对象就是不可能再使用的(对象已死)。
应用案例:Python、微软COM(Component Object Model)技术。
但主流的Java虚拟机中没有选用此算法来进行内存管理。因为有很多例外的情况,比如对象之间相互循环引用的问题就很难解决。
2.2 可达性分析算法
原理:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain)。如果某个对象到GC Roots间没有任何引用链相连,则此对象是不可能再被使用的。
可作为GC Roots的对象:
• 在虚拟机栈(栈帧中的本地变量表)中引用的对象;
• 在方法区中类静态属性引用的对象;
• 在方法区中常量引用的对象;
• 在本地方法栈中(即Native方法)引用的对象;
• Java虚拟机内部的引用;
• 所有被同步锁持有的对象;
• 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
2.3 是否真的死亡
需要注意的是,这里的finalize()方法只是演示整个过程,实际中并不会使用该方法,而是使用try-finally或其他方法,该方法忘记就好。
2.4 浅谈引用(强、软、弱、虚)
强引用:指在程序代码中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。在任何情况下,只要强引用关系存在,垃圾收集器永远不会回收被引用的对象。
软引用:用来描述一些还有用,但非必须的对象。在系统将要发生OOM异常时,会将被软引用关联的对象进行回收,如果回收后内存依旧不够,就抛出OOM异常。JDK1.2后,可使用SoftReference类型实现。
弱引用:也是描述非必须对象,但强度比软引用要弱。被弱引用关联的对象,在下一次垃圾收集发生时,直接被回收。JDK1.2后,可使用WeakReference类型实现。
虚引用:也称“幽灵引用”或“幻影引用”,没有任何意义,只是为了被设置虚引用关联的对象在被收集器回收时,收到一个系统通知。JDK1.2后,可使用PhantomReference类型实现。
3. 垃圾收集算法
本文介绍的算法属于追踪式垃圾收集(Tracing GC)
3.1 分代收集理论
主要分为两个分代假说:
• 弱分代假说:绝大多数对象都是朝生夕死;
• 强分代假说:越多次垃圾收集都未被收集的对象就越难被收集。
(1)这给设计垃圾收集器带来了一直的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(熬过垃圾收集的次数)分配到不同的区域之中存储。
(2)也划分出不同的回收类型:
• 部分收集(Partial GC):指目标不是完整收集Java堆的垃圾收集:
• 新生代收集(Minor GC):指目标只是新生代的收集;
• 老年代收集(Major GC):指目标只是老年代的收集。目前只有CMS收集器会有单独收集老年代的行为;
• 混合收集(Mixed GC):指目标是整个新生代和部分老年代的收集。目前只有G1收集器会有这种行为;
• 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
3.2 标记-清除算法
原理:标记所有需要被回收的对象(标记),然后统一回收(清除),反之也一样;
缺点:
• 执行效率不稳定。标记和清除的时间是随着Java堆中需要被回收的对象的数量的增加而增加,执行效率随之降低;
• 空间碎片化问题。标记-清除动作会产生大量不连续内存碎片,可能会导致分配内存空间时,找不到足够的连续空间而不得不提前触发另一次收集动作。
示意图:
3.3 标记-复制算法
原理:将内存分为大小相等的两块区域,每次只是用一块内存。当发生垃圾收集动作时,将目前块上存活的对象复制到另一块内存上,然后将当前块全部回收。
缺点:
• 如果有大量存活对象,复制过程会产生比较大的开销。当然,如果大部分都是要回收的对象,开销就比较小;
• 空间浪费太严重。可用内存变成了原来的一半。
用途:此算法被现在主流的Java虚拟机用于收集新生代。
改进:由于新生代中的对象绝大多数都朝生夕死(98%),所以提出了“Appel式回收”:将新生代分成一块较大的Eden空间和两块较小的Survivor空间(Eden:Survivor比例为8:1),分配内存使用一块Eden和一块Survivor, 当发生垃圾收集时,将存活对象复制到另一块Survivor上,然后对当前Eden空间和Survivor空间全部回收。当Survivor空间不足以容纳 一 次Minor GC之后存活的对象时,就需要依赖其他的内存区域(大多数为老年代)来进行分配(“逃生门”设计)。
示意图:
3.4 标记-整理算法
原理:标记动作与标记-清除算法一样,但后续是将所有存活对象向内存空一边进行移动,然后直接清除边界以外的内存。
用途:针对老年代。
缺点:由于老年代有大量对象存活,每次移动必须全程暂停用户应用程序才能进行,即“Stop The World”。但相比较出现碎片化的情况,这种代价可以接受。
示意图: