垃圾回收机制
Java虚拟机JVM提供了一种名为垃圾回收(GC)的机制,来帮助程序员实现自动的动态内存管理。其主要完成三件事:
1)判断哪些垃圾需要回收?
2)什么时候回收?
3)如何回收?
显而易见的是,程序计数器、虚拟机方法栈、本地方法栈都是线程私有的,其内存的分配与回收随线程的创建与结束而实现,是确定的。而Java堆与方法区则是不确定的,只有在程序实际运行时,才能知道有多少对象被创建了,这部分内存的分配与回收是动态的。
1. 判断对象的状态
1)引用计数算法:
其基本原理是在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
引用计数算法的优点在于原理简单、判定效率也不错,但有许多例外的情况需要考虑。
2)可达性分析算法(JVM采用):
其基本思想是通过一系列称为“GC Roots”的根对象作为起始节点,根据其引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到任一GC Roots都没有任何引用链相连,则称此对象为不可达,即该对象不可能再被使用。
在经过了可达性分析被标记为不可达的对象并不会立刻死亡,判断一个对象是否应该死亡需要两次标记过程:
第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。(PS:假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。)
注意:这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。因此已经在finalize()中拯救过自己的对象第二次将不可避免的会被回收。
第二次标记:如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中以执行finalize()方法,finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()中重新与引用链上的任何一个对象建立关联,那么在二次标记时其就会被移出“即将回收”的集合。
2. 垃圾收集算法
从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。以下介绍基于主流Java虚拟机使用的追踪式垃圾收集。
分代收集理论:
目前商业虚拟机的垃圾收集器,大多遵循了"分代收集"的理论进行设计,其基于以下三个分代假说:
1)弱分代假说:所有对象都是朝生夕灭的;
2)强分代假说:熬过越多次垃圾回收过程的对象就越难以消亡;
3)跨代引用假说:跨代引用相对于同代引用来说仅仅占少数。
由此得出的垃圾收集器的一致性设计原则:
收集器应将Java堆划分出不同的区域(一般会将Java堆划分为新生代与老年代),然后将回收对象依据其年龄(即对象熬过垃圾回收过程的次数)分配到不同的区域之中存储,然后垃圾收集器可以每次只回收其中某一个或者某些部分的区域。
名词统一定义:
新生代收集(Minor GC):只针对新生代区域的收集;
老年代收集(Major GC):只针对老年代区域的收集;
混合收集(Mixed GC):目标是整个新生代与部分老年代;
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集;
标记-清除算法:
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
缺点:执行效率不稳定,回收对象过多时效率较低;容易产生大量的内存碎片。
标记-复制算法(JVM新生代区域常用):
算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:仅能使用一半的内存空间。
Appel式回收:
Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%)。当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。
标记-整理算法(老年代常用):
算法中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
3. 垃圾收集器
Serial收集器:
一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的。
ParNew收集器:
ParNew收集器实质上是Serial收集器的多线程并行版本,主要功能的实现原理与Serial收集器一致。
Parallel Scavenge收集器
Serial Old收集器:Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
CMS收集器:是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现的。
Garbage First收集器: 一款主要面向服务端应用的垃圾收集器。
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
4. 内存分配与回收策略
JVM的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。
本例的回收策略是基于HotSpot虚拟机,使用 Serial + SerialOld 客户端默认收集器组合下的内存分配与回收策略:
1)对象优先在Eden分配:
当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。注:Serial将新生代分为一个Eden区与两个Survivor区。
2)大对象直接进入老年代:
大对象(指需要大量连续内存空间的Java对象,典型的大对象如很长的字符串,或者元素数量很庞大的数组)。HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
3)长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置
4)动态对象年龄判定
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
5)空间分配担保
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。