JVM垃圾清理机制详解
jvm内存结构中有一块地方叫做堆内存,里面存放着我们应用创建的对象,但是我们堆内存有限,对象在运行的时候持续创建,jvm有垃圾清理机制来清理对象确保堆内存的可用空间。
清理流程
从上图可以看出我们的执行引擎会负责在需要垃圾处理的时候起一个GC垃圾收集线程对堆内存中的年轻代和老年代进行垃圾清除。
1、我们创建的对象一般都放在eden区,大对象则直接放到老年代。
2、当eden区域填满时,触发一次垃圾回收事件。 将存活的对象放到s0区域,对象年龄加一,eden区域没有引用的对象全部被删除。s0和s1有一个是没有对象的‘空闲区域’,一个是有对象的活动区域,然后s0和s1在再在触发Minor gc后,先采用根搜索算法标记对象,然后把活着的对象全部复制到另一半空闲区间上,复制算法的“复制”就来自这一操作。复制到另一半区间的时候,严格按照内存地址依次排列要存放的对象,然后一次性回收垃圾对象。这样原来的空闲区间在GC后就变成活动区间(后面每经过一次Minor gc,活动区域和空闲区域进行轮转),而且内存顺序齐整美观。原来的活动区间在GC后就变成了完全空的空闲区间,等待下一次GC把活的对象被copy进来。
3、在s0和s1空间中的对象每经过一次Minor gc存活下来后年龄加一,年龄到达15岁之后转移到老年代。
4、当老年代满了之后触发MajorGC或者Full gc
5、这些清理动作都是由GC收集器去做,下面的内容会讲到收集器
MinorGC、MajorGC、FullGC触发条件
MinorGC
当年轻代(Eden区)满时就会触发 Minor GC,这里的年轻代满指的是 Eden区满。Survivor 满不会触发 Minor GC 。对于大部分应用程序,Minor GC 操作时应用程序停顿导致的延迟都是可以忽略不计的。大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
MajorGC
当老年代满时会触发MajorGC,只有CMS收集器会有单独收集老年代的行为,其他收集器均无此行为。而针对新生代的MinorGC,各个收集器均支持。总之,单独发生收集行为的只有新生代,除了CMS收集器,都不支持单独回收老年代。
FullGC
FullGC是针对新生代,老年代和方法区(元空间)的垃圾收集。FullGC产生的条件:
(1)调用System.gc时,系统建议执行Full GC,但是不一定会执行 。
(2)老年代空间不足。
(3)方法区空间不足,类卸载(类卸载三个条件)。
(4)通过 Minor GC 后进入老年代的空间大于老年代的可用内存
(5)内存空间担保。
GC算法
标记-清除算法
该算法分两步执行:
1) 标记Mark:从GC ROOTS开始,遍历堆内存区域的所有根对象,对在引用链上的对象都进行标记。这样下来,如果是存活的对象就会被做了标记,反之如果是垃圾对象,则没做有标记。GC很容易根据有没有被做标记就完成了垃圾对象回收。
2) 清除Sweep:遍历堆中的所有的对象(标记阶段遍历的是所有根节点),找到未被标记的对象,直接回收所占的内存,释放空间。
评价:
【优点】没有产生额外的内存空间消耗,内存利用率高。
【缺点】效率低,清除阶段要遍历所有的对象;回收的垃圾对象是在各个角落的,直接回收垃圾对象,导致存在不连续的内存空间,产生内存碎片。
标记-清除算法操作的对象是【垃圾对象】,对于活着的对象(被标记的对象),它则直接不理睬。
复制算法
内存一分为二, 当内存空间不足时触发GC,先采用根搜索算法标记对象,然后把活着的对象全部复制到另一半空闲区间上,复制算法的“复制”就来自这一操作。复制到另一半区间的时候,严格按照内存地址依次排列要存放的对象,然后一次性回收垃圾对象。
评价:
【优点】GC后的内存齐整,不产生内存碎片。
【缺点】GC要使用两倍的内存,或者说导致堆只能使用被分配到的内存的一半,这个算法对空间要求太高!如果存活的对象较多,则意味着要复制很多对象并且要维护大量对象的内存地址,所以存活的对象数量不能太多,否则效率也会很低。
复制算法复制移动的对象是【活着的对象】,对于垃圾对象(不被标记的对象)则直接回收。
标记-整理算法
这个算法则是对上面两个算法的综合结果。也分为两个阶段:
1)标记:这个阶段和标记-清除Mark-Sweep算法一样,遍历GC ROOTS并标记存活的对象。
2)整理:移动所有活着的对象到内存区域的一侧(具体在哪一侧则由GC实现),严格按照内存地址次序依次排列活着的对象,然后将最后一个活着的对象地址以后的空间全部回收。
评价:
【优点】内存空间利用率高,消除了复制算法内存减半的情况;GC后不会产生内存碎片。
【缺点】需要遍历标记活着的对象,效率较低;复制移动对象后,还要维护这些活着对象的引用地址列表。
jdk1.8垃圾常用收集器
常用垃圾收集器简述
1、Serial和Serial old:年轻代使用复制算法,暂停用户线程到safepoint,使用单一线程进行垃圾收集。老年代使用标记-整理算法,暂停用户线程到safepoint,也是使用单一线程进行垃圾收集。 2、ParNew和ParNew Old:年轻代使用复制算法,暂停用户线程到safepoint,使用多条线程进行并行垃圾收集。老年代使用标记-整理算法,暂停用户线程到safepoint,也是使用多条线程进行并行垃圾收集。 3、ParNew和CMS(ConcurrentMarkSweep:并行标记清除):年轻代使用复制算法,暂停用户线程到safepoint,使用多条线程进行并行垃圾收集。老年代采用标记-清除算法,初始标记(产生STW:时间很简短),标记GCRoots能直接关联到的对象,时间很短。并发标记(不产生STW,但是),从GCRoots能直接关联到的对象进行整个对象图的遍历过程,进行GCRoots Tracing(可达性分析)过程,时间很长。重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。并发清除,回收内存空间,时间很长。并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。
垃圾收集器详解
Serial(串行)收集器(jdk1.1、1.2版本)
最基本、发展历史最久的收集器,这个收集器是一个采用复制算法的单线程的收集器,单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。后者意味着,在用户不可见的情况下要把用户正常工作的线程全部停掉,这对很多应用是难以接受的。不过实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效。用户桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代停顿时间在几十毫秒最多一百毫秒,只要不是频繁发生,这点停顿是完全可以接受的。Serial收集器运行过程如下图所示:
说明:1. 需要STW(Stop The World),停顿时间长。2. 简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。
Serial Old收集器
Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理算法”,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
ParNew(并行)收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括使用的也是复制算法。ParNew收集器除了多线程以外和Serial收集器并没有太多创新的地方,但是它却是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作(看图)。CMS收集器是一款几乎可以认为有划时代意义的垃圾收集器,因为它第一次实现了让垃圾收集线程与用户线程基本上同时工作。ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于线程交互的开销,该收集器在两个CPU的环境中都不能百分之百保证可以超越Serial收集器。当然,随着可用CPU数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。ParNew收集器运行过程如下图所示:
Parallel Scavenge收集器
Parallel Scavenge收集器也是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器,但是它的特点是它的关注点和其他收集器不同。介绍这个收集器主要还是介绍吞吐量的概念。CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是打到一个可控制的吞吐量。所谓吞吐量的意思就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总运行100分钟,垃圾收集1分钟,那吞吐量就是99%。另外,Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。
停顿时间短适合需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK 1.6之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合。运行过程如下图所示:
CMS收集器
CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步:
初始标记,标记GC Roots能直接关联到的对象,时间很短。
并发标记,进行GC Roots Tracing(可达性分析)过程,从GC Roots直接关联的对象开始遍历整个对象图,耗时较长但是不会造成STW。
重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(并发标记时可能发生用户线程改变对象引用的情况),时间较长。
并发清除,清理掉标记阶段以及判断死亡的对象,不需要移动对象,这个阶段用户线程和收集器线程可以并发进行。
其中初始标记和重新标记依旧需要STW(Stop the world).
其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。运行过程如下图所示:
G1收集器(JDK1.9)
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。