垃圾回收(GC)一直是java语言的重中之重。
1 对象状态鉴别
1.1 标记对象是否可回收一般有两种算法:
- 引用计数算法:给每个对象添加一个引用计数器,当引用一次时+1,当引用时效时-1,当计数器为0时即可回收。该算法最大的缺点是当多个对象相互循环引用时将用不释放。
- 可达性分析算法:将“GC Roots”作为起点向下搜索,搜索所走过的路径称为引用链,当对象与GC roots没有任何连接时即可释放。GC Roots包含如下四种对象:虚拟机栈(战阵中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。
1.2 引用
在JDK1.2以前,java中应用的定义很狭义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用;在JDK1.2之后,java对引用的概念进行了扩充分为如下四类:
- 强引用:代码中普遍存在的,如“Object obj = new Object()”这类,只要强引用存在,垃圾回收器永远不会回收被引用的对象。
- 软引用:用来描述一些还有用但并非必须的对象,在系统发生内存溢出之前,将这些对象列入可回收队列进行第二次回收。
- 弱引用:用来描述非必需的对象,强度比软引用稍弱,无论当前内存是否充足,弱引用关联的对象只能生存到下一次垃圾回收之前。
- 虚引用:最弱的一种引用关系,完全不会对关联对象的生存时间产生影响,也无法通过虚引用获取一个对象的实例,完全是为了在这个对象被回收时收到一个系统通知。
1.3 两次确认(不推荐使用finalize()方法)
在可达性分析中不可达的对象也不是一定被回收,一个对象被宣判死刑,至少要经历两次标记过程:如果对象在进行可达性分析时发现没有与GC Roots相连,它将会被将第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,将视为“没必要执行”,立即回收。
当这个对象被判定有必要执行finalize()方法时,这个对象将会放置在F-Queue队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程触发对象的finalize()方法,但不承诺保证方法运行结束。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queuezhongde对象进行第二次小规模标记,只要重新与GC Roots引用链上的任何一个对象建立关联即可完成救赎,否则将被回收。
值得注意的是,任何一个对象的finalize()方法只会被系统自动调用一次,此外finalize()方法并不推荐使用,而是应该更多的使用try-finally等其他方式。
1.4 方法区回收
大多数人可能认为方法区(HotSpot虚拟机中的永久代)是没有垃圾收集的,其不然。永久代的垃圾回收只要收集“废弃常量”和“无用的类”两部分。
- 废弃常量:与回收堆中的对象相似,以常量池中字面量的回收为例,如果一个字符串“abc”已经进入了常量池中,但当前没有任何一个String对象引用了“abc”常量,如果发生内存回收,而且必要时将会将“abc”这个常量清除出常量池。常量池中的其他类、接口、方法的符号引用也与此类似。 无用的类:无用的类判断条件相对苛刻很多,只有同时满足如下三个条件才可算为无用的类。
- 该类的所有实例都已经被回收,即java堆中不存在该类的任何实例
- 加载该类的classLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被应用,无法在任何地方通过反射方位该类的方法
2 垃圾收集算法
- 标记-清除算法 首先标记需要回收的对象,然后在标记完成后统一回收所有被标记的对象。具有两个缺点,其一,效率问题,标记和清楚两个阶段效率都不高;其二,空间问题,标记清除之后会产生大量不连续的碎片。(白色区域为未使用,绿色区域为存活对象,红色区域为可回收)
- 复制算法 为了解决效率问题进化而来。将可用内存划分为大小相等的两块,每次使用其中的一块,当这一块内存用完就将还存活的对象复制到另一块内存上,然后将已使用的内存一次性清空,这样不必考虑内存碎片问题。现在的商业虚拟机都采用这种收集方法回收新生代内存,俱IBM公司调研新生代中的对象98%是“朝生夕死”的,所以并不需要1:1划分,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一块Survivor。当回收时,Eden和Survivor中还存活的对象一次性复制到另一块Survivor中,并其清理掉Eden和刚才使用的Survivor。在HotSpot中,Eden和Survivor的比例大小是8:1。
- 标记-整理算法 同样首先标记需要回收的对象,但后续步骤是让存活的对象都向一端移动,然后直接清理掉边界外的内存。(白色区域为未使用,绿色区域为存活对象,红色区域为可回收)
- 分代收集算法 根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代,根据各个年代的特点采用适当的手机算法。新生代采用复制算法,老年代采用“标记-清理”或者“标记-整理”算法。
3 内存分配与回收策略
java技术体系中所提倡的自动内存管理最终可归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。对象的内存分配往大方向讲就是在堆上分配,主要是在新生代的Eden区,如果启动了本地线程分配缓冲,将按线程优先在TLBA上分配。内存分配具有几条普遍的分配规则:
- 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
- 大对象直接进入老年代,大对象就是需要大量连续空间的java对象,最典型的是长字符串或数组
- 长存活期的对象将进入老年代,对象经过一次Minor GC存活并被Survivor容纳,该对象年龄置为1,以后没熬过一次Minor GC,年龄就加1,当到一定程度就会晋升到老年代
- 动态对象年龄判定,虚拟机并不是永远要求只有到年龄才能晋升老年代,如果相同年龄所有对象的大小总和大于Survivor空间一半,则其他大于该年龄的对象可以直接进入老年代
- 空间分配担保,在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对总空间,如果条件成立,Minor GC可以确保安全,否则就可能需要一次Full GC
Minor GC 和 Full GC 区别
- 新生代GC(Minor GC):发生在新生代的垃圾收集动作,相对较频繁,回收速度也较快
- 老年代GC(Major GC / Full GC):发生在老年代的GC,出现了Major GC,经常会伴有至少一次Minor GC,Major GC速度一般比Minor GC慢10倍以上
4 HotSpot的GC算法实现
- 枚举根节点 可达性分析对于执行时间的敏感体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行(这里的一致性是指整个分析过程中整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,以保证分析结果准确性),这点也是GC进行时必须停顿所有java执行线程(stop the world)的一个重要原因。在Hotspot虚拟机中,为了提升可达性分析效率,当执行系统停顿下来后,通过一组称为OopMap的数据结构来直接得知那些地方存放着对象引用,以便快速统计对象状态。
- 安全点 在OopMap的帮助下,HotSpot可以准确快速的完成GC Roots枚举。HotSpot没有为每条机器指令都生成OopMap,而是在某些特定的位置记录这些信息,即为安全点;也只有到达安全点才能停顿。如何在发生GC时所有线程都跑到最近的安全点停顿下来,有两种方案可选:其一,抢断式中断(基本被抛弃),首先把所有线程中断,如果发现有线程中断的地方不在安全点,就让线程恢复并跑到安全点;主动式中断,当GC需要中断时,不直接对线程操作,而是设置与安全点位置重合的一个标识,每个线程执行时主动去轮训这个标识,当发现标识为true时将自己中断挂起。
- 安全区域 安全点可以保证程序执行时在不长的时间内就会进入安全点,但是对于处于sleep或blocked等状态的没有分配CPU时间的线程无法进入安全点,这时候需要安全区域来解决。安全区域指一段代码片段中,引用关系不会发生变化,这个地方区域中的任意地方开始GC都是安全的。当线程执行到安全区域的代码中时,将标识自己进入安全区域,这样JVM发起GC时就就不用管处在安全区域的进程了。
5 收集器简介