深入理解Java虚拟机 4.JVM垃圾回收机制详解
JVM处理类加载机制之外,另一个重要的机制就是声名远播的垃圾回收机制。对于Java来说,在虚拟机自动内存管理机制下,不在需要向C++程序员那样,手工去编写delete/free内存操作,这样就很大程度上避免了因操作不当引起的内存泄漏和内存溢出等问题。在Java中,JVM会在适当的时候,对那些需要回收的对象进行垃圾回收操作,释放内存。
本文将针对刚才提出的三个问题:哪些对象已经死亡,需要进行垃圾回收?什么时间进行垃圾回收?如何进行垃圾回收?以及当前一些主流垃圾收集器进行详细介绍。
1.哪些对象已经死亡,需要进行垃圾回收?
在垃圾回收机制中,首先要做的就是判断哪些对象已经死亡不再被使用,需要进行垃圾回收?
在判断一个对象是否死亡,不再被引用,分为两个阶段:第一阶段就是对象是否可用,通过引用计数算法和可达性分析算法进行判断;第二阶段就是通过第一阶段判断帅选出来的不可用对象是否真正死亡,需要进行垃圾回收。通过对象的引用类型以及finalize方法进行判断。下面进行详细介绍。
1.1哪些对象不再被引用
引用计数算法:给对象添加一个引用计数器,对象每当被引用时,计数器加一,对象的引用失效时,计数器减一。任何时候,当计数器的值为0时,这个对象就是不可能再被引用的对象。
引用计数器的优点是实现简单,判断效率高。缺点是很难解决对象之间的循环依赖问题,这样计数器始终不为0,GC永远无法进行对象回收。
可达性分析算法:通过一些列被称为“RootGC”的对象作为起点,从这些节点开始向下搜索,所走过的路径被称为引用链。如果一个对象到GCRoot根节点没有任何引用链相连,则该对象就是不可用的对象。
1.2 对象引用类型
对象引用分为四种:强引用、软引用、弱引用、虚引用。不管是通过引用计数算法或者可达性分析算法判断对象是否存货都与对象引用有关。
强引用:类似Object obj = new Object(),这样的引用就是强引用。只要对昂强引用还存在,垃圾收集器就无法回收此对象。
软引用:用来描述一些还有用但非必须的对象。对象如果是软引用类型,再有在内存不足的情况的才会进行垃圾回收;如果内存充足,不会进行回收。
弱引用:如果对象的引用关系是弱引用,不管内存是否充足,在发生垃圾收集时,都会将对象进行回收。
虚引用:表示可有可无的引用关系,如果对象是虚引用类型,不会印象对象回收。
1.3 finalize方法进行对象第二次帅选,对象是否真正死亡
通过引用计数算法或者可达性分析算法帅选出来的“不可用”的对象,也并非非死不可。要真正宣告一个对象死亡,还需要对帅选出来的“不可用”对象进行第二次判断。
第一阶段:“不可用”对象是否有必要执行finalize方法。如果对象没有覆盖finalize方法或者finalize方法已经被执行过,都将判定该对象没有必要执行finalize方法,那么该对象就是已经死亡的对象,可以进行垃圾回收。
第二阶段:如果通过第一阶段判定的兑现有必要执行finalize方法,则该对象会被放到一个队列中,稍后JVM会创建一个线程,去执行finalize方法。如果通过执行finalize方法之后,对象重新与GCRoot引用链关联,则该对象就会变成可用对象,不能进行垃圾回收。如果通过执行finalize方法之后,对象仍然未能与引用链关联,则该对象就判定为死亡,可进行垃圾回收。
2.什么时间进行垃圾回收?
死亡对象的回收时间主要根据对象的引用类型,JVM的内存空间是否充足,以及写一次垃圾回收线程的执行时间有关。关于引用类型和JVM内存空间是否充足,详见上面介绍。垃圾回收并不是当判定了对象已经死亡就会立即进行垃圾回收,而是要等到下次垃圾回收线程的执行。
3.如何进行垃圾回收?
垃圾收集算法根据不同的虚拟机,操作内存的方法有不尽相同,同时垃圾收集算法也有所不同。所以下面的几种算法只是对垃圾收集算法的思想以及其发展过程做详细介绍。
3.1 标记-清除算法
标记清除算法分为两个阶段:第一阶段标记,将所有死亡的对象标记出来;第二阶段清除,将所有标记的对象统一回收。是最基础的收集算法,因为其他几种算法都是基于这种思想进行的改进。
标记-清除算法的缺点:
1.效率问题,标记,清除两个过程的料率都不高。一方面因为它首先要将所有的死亡对象标记出来,在统一回收,这本身就是一个漫长的过程。
2.空间问题:标记清除之后会存现大量不连续的内存碎片,空间碎片太多会导致后面为大对象分配内存是,找不到足够大的连续的内存而再次触发一次垃圾回收。
3.2 复制算法
复制算法是将内存划分为大小相等的两块内存,每次只使用其中的一块内存。当第一块内存使用完了,就将依然存货的对象复制到另一块内存中,然后将之前的第一块内存统一进行一次回收清除操作。每次重复相同的动作。改良的复制算法是用来回收新生代中对象,将两块内存按照8:1的比例进行拆分,因为新生代中的对象死亡率高,每次清除之后剩余存货的对象可能只占到10%,大多数都会死亡。这样也就大大提高了内存的使用率。
复制算法的优点:实现简单,运行高效,不需要考虑碎片问题。缺点就是每次只能使用一般内存,缩小了可用内存空间。
3.3 标记-整理算法
标记-整理算法同标记-清除算法相同,分为两个阶段:第一阶段为标记,标记出所有死亡的对象;第二阶段整理清除,将所有存活的对象都向一段移动,然后直接清理掉边界以外的内存。
3.4 分代收集算法
分代收集算法是根据对象的存活周期的不同而将堆内存划分为新生代和老年代。然后根据各个年代不同的特点采用不同的收集算法。在新生代由于对象死亡率高,采用复制算法。而在老年代中,由于对象比较稳定,存活率高,采用标记-整理或者标记-清除算法。
4.垃圾收集器有哪些?
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
图中展示了其中作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以相互搭配是用。图中黑色横线上部分表示新生代收集器,黑色横线下部分表示来年代收集器。
4.1 Serial收集器
Serial收集器时最基本的,同时也是一个单线程的收集器。“单线程”的意义并不是说明他只会使用一个CPU或是一条收集线程去完成垃圾收集工作,而是有多个线程,但是同一时刻只能有一个线程运行。因为它在进行垃圾收集的时候,必须暂停其他所有的工作线程。
优点:在JDK1.3之前是唯一的新生代垃圾收集器。因为单线程工作模式,没有线程交互的开销,是简单、高效的(与其他单线程收集器相比)的收集器。可与CMS收集器配合使用。
缺点:单线程工作模式,使得其他工作线程阻塞,工作线程会因为垃圾收集线程的运行而出现短暂的、一定频率的停顿。
对于在桌面应用场景中,Serial收集器是不错的新生代垃圾收集器的选择。因为在桌面应用场景中,分配给虚拟机的内存比较小,垃圾收集线程收集所占用的时间会特别小,对于工作线程来说,停顿的时间也就特别短暂。
4.2 ParNew收集器
ParNer收集器其实是Serial收集器的多线程版本,使用多线程进行垃圾收集。是除了Serial收集器之外唯一的可以与CMS收集器配合工作的,是Server模式下的虚拟机中首选的新生代垃圾收集器。
4.3 Parallel Scavenge 收集器
Parallel Scavenge 同样是一个新生代、采用复制算法的,而且是并行的多线程的垃圾收集器。它的目标是达到一个可控制的吞吐量(吞吐量= 运行用户代码的时间/运行用户代码的时间+垃圾收集的时间)。提供两个参数MaxGCPauseMillis、GCTimeRatio来精确控制吞吐量,两个参数分别用于控制最大垃圾收集停顿时间和直接设置吞吐量大小。
MaxGCPauseMillis:允许的值是一个大于0的毫秒数,收集器将尽可能的保证内存回收花费的时间不超过设定值。并不是该值设置的稍小一点就能是的系统的垃圾收集速度变得更快,GC停顿的时间是以牺牲吞吐量和新生代空间来换取的。如收集500M的新生代垃圾,原来10秒收集一次,每次停顿100秒。现在MaxGCPauseMillis的值设置为5秒,则会变成每5秒收集一次,每次停顿70毫秒。停顿时间在下降,但是同时吞吐量也在下降。
GCTimeRatio:允许的值为0~100闭区间的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。
由于与吞吐量的关系密切,Parallel Scavenge收集器也经常被称为吞吐量有优先的收集器。
4.4 Serial Old 收集器
Serial Old是收集器是Serial收集器的老年代版本,同样是一个单线程收集器,采用标记-整理算法。
4.5 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
4.6CMS收集器
CMS收集器是一种以获取最短停顿时间为目标的收集器,采用标记-清除算法实现的,且基本上实现了回收线程与用户线程并发执行、同时工作的低停顿的收集器。收集过程主要包括如下4个步骤:初始标记、并发标记、重新标记、并发清除。
其中初始标记和重新标记两个步骤仍然需要停顿。但是时间相比之前的收集器来说,停顿时间较短。初始标记只是标记GC Root 能直接关联到的对象,速度很快;并发标记就是进行GC Root Tracing的过程;重新标记主要是为了修正因并发标记过程中用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记稍长一些,远比并发标记的时间要短;并发清除就是采用标记-清除算法清除掉已经标记的垃圾对象,耗时较长。
由于在整个垃圾收集过程中,耗时最长的并发标记和并发清除阶段都是垃圾线程可与用户线程并发工作,所以从这个角度来说,CMS收集器垃圾回收是并发执行的。
优点:并发执行、低停顿
缺点:
1.垃圾收集线程与用户线程并发执行,对CPU资源的使用比较敏感。当垃圾收集线程占用一定数量的系统资源时,会导致用户线程的吞吐量降低。
2.因为并发清理阶段,用户线程还在运行,且并发时间段内的垃圾对象并未被标记。所以导致到并发清理阶段用户线程产生的垃圾是无法进行回收的。需要等待下次垃圾回收线程的执行。
3.因采用标记-整理算法,所以回到大量的内存碎片。而大量的内存碎片会导致某种情况下,需要为大对象分配内存时找不到连续的足够大的内存空间。往往会触发一次新的垃圾回收过程。这可能就需要其他一些手段来处理内存碎片的问题。如CMS收集器提供的通过参数来控制是否开启内存碎片合并整理等,但往往会伴随其他一些问题的出现。入碎片整理过程本身是无法并发的,碎片内存问题没有了,但是停顿时间不得不变长。
4.7 G1收集器
G1收集器是目前收集技术最前沿的成果之一,是一个成熟的,收集范围包括整个新生代或者老年代的收集器。使用G1收集器时,他将Java堆划分为多个大小相等的独立区域,虽然还保留着新生代和老年代的概念,但新生代和来年代不在是物理隔离的了,他们都是一部分区域的集合,这是与其他收集器最大的不同点。G1具有以下特点:
1.并发和并行:充分利用CPU、多核资源,使得GC线程和用户线程实现真正的并发、并行执行。
2.分代手机:虽然不需要想其他收集器那样,需要配合不同的年代的收集器分别对新生代、老年代进行垃圾回收,但是仍然保留着分代的概念。G1收集器可以采用不同的方式对新生代和老年代进行回收。
3.空间整合:G1收集器从全局来看采用标记-整理算法,从局部来看采用复制算法。但是这两种算法中的任何一种,都不会产生内存空间碎片。收集后的内存空间仍然是规整的可用内存,分配大对象内存时不会因为找不到连续的足够大的内存空间而不得不触发新一轮的内存回收。
4.可预测的停顿:同CMS不同的是除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。所谓实现的可预测模型,是因为它可以有计划的避免在真个Java堆这种进行全区域的垃圾收集,换句话说就是可以选择部分区域进行垃圾回收。
G1收集器的收集过程大致可划分为以下四个步骤:
1.初始标记:同CMS的初始标记相同,只是标记GC Root能直接关联到的对象,次过程需要停顿用户线程,但是时间很短。
2.并发标记:耗时较长,但可与用户线程并发执行。通过对堆中对象的可达性分析,找出存货对象。
3.最终标记:修正在并发标记阶段并发运行的用户线程产生变动的那一部分已经被标记的对象。
4.帅选回收:根据用户建立的可预测停顿时间模型,制定合适的回收方案。