垃圾收集器(GC,Garbage Collector)是和具体JVM实现紧密相关的,不同厂商(IBM、Oracle),不同版本的JVM,提供的选择也不同。
1、Serial GC,它是最古老的垃圾收集器,“Serial”体现再其手机工作是单线程的,并且再进行垃圾收集过程中,会进入“Stop-The-World”状态。当然,其单线程设计也意味着精简的GC实现,无需维护复杂的数据结构,初始化也简单,所以一直是Client模式下JVM的默认选项。从年代的角度,通常将其老年代实现单独称为Serial Old,它采用了标记-整理(Mark-Compact)算法,区别于新生代的复制算法。Serial GC的对应JVM参数是:-XX:+UseSerialGC.
ParNew GC,是个新生代GC实现,它实际上是Serial GC的多线程版本,最常见的应用场景是配合老年代的CMS GC工作,对应参数是:-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
CMS(Concurrent Mark Sweep)GC,基于标记-清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于Web等反应时间敏感的应用是非常重要,一直到今天,仍然又很多系统使用CMS GC.但是,CMS采用的标记-清除算法,存在着内存碎片化问题,所以难以避免再长时间运行等情况下发生full GC,导致恶劣的停顿。林外,既然强调了并发(Concurrent),CMS会占用更多CPU资源,并和用户线程争抢。
Parrallel GC,再早期JDK8等版本中,它是server模式JVM的默认GC选择,也被称作是吞吐量优先的GC.它的算法和Serial GC比较相似,尽管实现要复杂的多,其特点是新生代和老年代GC都是并行进行的,在常见的服务器环境中更加高效,开启选项是:-XX:+UseParallelGC.
G1 GC这是一种兼顾吞吐量和停顿时间的GC实现,是Oracle JDK9以后的默认GC选项。
G1可以直观的设定停顿时间的目标,相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
G1 GC仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个region。Region之间是是复制算法,但是整体上可看作是标记-整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当java堆非常大的时候,G1的优势更加明显。G1吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时CMS在JDK9中被标记为废弃,所以G1 GC值得深入掌握。
垃圾收集的原理和基础概念
基础概念:
自动垃圾收集的前提是清楚哪些内存可以被释放,主要是两个方面,最主要部分就是对象实例,都是存储在堆上的;还有就是方法区中的元数据等信息,例如类型不再使用,卸载该java类似乎是很合理的。
对于对象实例收集,主要是两种基本算法:引用计数、可达性分析
1、引用计数:顾名思义,就是为对象添加一个引用计数,用于记录对象被引用的情况,如果计数为0,即表示对象可回收,这是很多语言的资源回收选择,例如因人工只能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。具体那种最优是要看场景的,业界又大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
2、java选择的是可达性分析,java的各种引用关系,在某种程度上,将可达性问题还进一步复杂化。简单来说就是将对象及其引用关系看作一个图,选定活动的对象作为GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。JVM会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,作为GC Roots。方法区无用元数据的回收比较复杂,一般来说初始化类加载器加载的类型是不会进行类卸载的;而普通的类型的卸载,往往是要求相应自定义类加载器本身被回收,所以大量使用动态类型的场合,需要防止元数据区(或者早期的永久代)不会OOM.
常见的垃圾收集算法:
复制算法:
将活着的对象复制到to区域,拷贝过程中将对象顺序放置,就可以避免内存碎片化。这么做的代价是:既然要进行复制,既要提前预留内存空间,有一定的浪费;另外,对于G1这种分拆成为大量regioin的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销。
标记-清除算法:
首先进行标记工作,标识出所有要回收的对象,然后进行清除。这么做除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,这就导致其不适合特别大的堆;否则,一旦出现Full GC,暂停时间可能根本无法接受。
标记-整理
类似于标记-清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。
垃圾收集过程的理解
1、java应用不断创建对象,通常都是分配在Eden区域,当其空间占用达到一定阈值时,出发minor GC。仍然被引用的对象(绿色方块)存活下来,被复制到JVM选择的Survivor区域,而没有被引用的对象(黄色方块)则被回收。注意,我给存活对象标记了“数字1”,则是为了表明对象的存活时间。
2、经过一次Minor GC,Eden就会空闲下来,知道再次达到Minor GC触发条件,这时候,另外一个Survivor区域则会成为to区域,Eden区域的存活对象和From区域对象,都会被复制都to区域,并且存活的年龄计数会被加1.
3、类似第二部的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升过程,如下图所示,超过阈值的对象会被晋升到老年代,这个阈值时可以通过参数指定:-XX:MaxTenuringThreshold=<N>
后面就是老年代GC,具体取决于选择的GC选项,对应不同的算法。下面是一个简单标记-整理算法过程示意图,老年代中的无用对象被清除后,GC会将对象进行整理,以防止内存碎片化。
通常会把老年代GC叫做Major GC,将对整个堆进行的清理叫作Full GC,但是这个也没有那么绝对,因为不同的老年代GC算法其实表现差异很大,例如CMS,“concurrent”就体现在清理工作是与工作线程一起并发运行的。
网友评论