JVM垃圾回收算法和收集器

时间:2023-01-02 09:09:41
一. JVM垃圾回收算法 1.引用计数器算法: 引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。 引用计数器实现简单,效率高;但是(1)不能解决循环引用问问题(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时(2)每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后,这个算法已经不再使用了。
2.根搜索方法 根搜索方法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain), 当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的。 GC Roots对象包括: a) 虚拟机栈(栈帧中的本地变量表)中的引用的对象。 b) 方法区域中的类静态属性引用的对象。 c) 方法区域中常量引用的对象。 d) 本地方法栈中JNI(Native方法)的引用的对象。 了解了JVM是怎么确定对象是“垃圾”之后,进入正题,让我们来看看垃圾回收的算法。
1.复制算法(Copying) 复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。 复制算法实现简单运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高 现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存默认大概是8:1。 垃圾回收前: JVM垃圾回收算法和收集器 垃圾回收后: JVM垃圾回收算法和收集器
2.标记—清除算法(Mark-Sweep) 标记—清除算法包括两个阶段:“标记”和“清除”。 在标记阶段,确定所有要回收的对象,并做标记。 清除阶段,将标记阶段确定不可用的对象清除。 标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。 垃圾回收前: JVM垃圾回收算法和收集器 垃圾回收后: JVM垃圾回收算法和收集器
3.标记—整理算法(Mark-Compact) 标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。 标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代 垃圾回收前: JVM垃圾回收算法和收集器 垃圾回收后: JVM垃圾回收算法和收集器 4.分代收集(Generational Collection) 分代收集是根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。 新生代采用复制算法,老年代采用标记—整理算法(个人感觉也不一定,如果老年代采用CMS收集器,算法就是标记-清除算法) 垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。上面介绍的只不过是基本思想。
二. JVM垃圾收集器 垃圾收集器就是收集算法的具体实现,不同的虚拟机会提供不同的垃圾收集器。并且提供参数供用户根据自己的应用特点和要求组合各个年代所使用的收集器。 本文讨论的收集器基于Sun Hotspot虚拟机1.6版。 下图中展示了jdk1.6中提供的6种作用于不同年代的收集器,两个收集器之间存在连线的话就说明它们可以搭配使用。没有最好的收集器,也没有万能的收集器,只有最合适的收集器。 从Serial收集器到Parallel收集器,再到CMS收集器, G1收集器,用户线程的停顿时间在不断缩短,但是仍然没有办法完全消除。 JVM垃圾回收算法和收集器 1. Serial收集器 单线程收集器,使用复制收集算法,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World),直到收集结束,虚拟机运行在Client模式时的默认新生代收集器。  收集过程:暂停所有线程 算法:复制算法 优点:简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器没有现成交互的开销。 应用:Client模式下的默认新生代收集器 场景:在堆比较小的情况下,一般停顿时间很短,是可以使用这种收集器的。
新生代Serial与年老代Serial Old搭配垃圾收集过程图: JVM垃圾回收算法和收集器 2. ParNew收集器 ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。 ParNew收集器是许多运行在server模式下的虚拟机中首选的新生代收集器(一个原因是在除了serial收集器外,目前只有它能与CMS收集器配合使用)。 ParNew收集器是使用-XX:+UseConcMarkSweepGC选项的默认新生代收集器;也可以用-XX:+UseParNewGC选项来强制指定它。 ParNew收集器在单CPU环境中不比Serial效果好,甚至可能更差,两个CPU也不一定跑的过,但随着CPU数量的增加,性能会逐步增加。 默认开启的收集线程数与CPU数量相同。在CPU数量很多的情况下,可以使用-XX:ParallelGCThreads参数来限制线程数。 收集过程:与用户线程并发 算法:复制算法 优点:在CPU多的情况下,拥有比Serial更好的效果。单CPU环境下Serial效果更好 应用:许多运行在Server模式下的虚拟机中首选的新生代收集器
3. Parallel Scavenge收集器 同ParNew一样是使用复制算法的新生代并行多线程收集器。 Parallel Scavenge的特点是它的关注点与其他收集器不同, CMS等收集器的关注点尽可能地缩短垃圾收集时用户线程的停顿时间 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,也被称为吞吐量优先收集器 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。 高吞吐量和停顿时间短的策略相比,主要强调任务更快完成,适用于后台运算而不需要太多交互的任务;而后者强调用户交互体验。 
Parallel Scavenge提供两个参数精确控制吞吐量, -XX:MaxGCPauseMillis控制最大垃圾收集停顿时间和-XX:GCTimeRatio设置吞吐量大小  1).MaxGCPauseMillis允许的值是一个大于零0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值 GC停顿时间缩小是以牺牲吞吐量新生代空间来换取的,也就是要使停顿时间更短,需要使新生代的空间减小,这样垃圾回收的频率会增加,吞吐量也降下来了。  2).GCTimeRatio的值是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率默认为99,则允许最大GC时间就占总时间的1/(1+99). 3).-XX:+UseAdaptiveSizePolicy,打开GC自适应调节策略会自动设置新生代大小、调整Eden与Survior区的比例、晋升老年代对象年龄,新生代大小等细节参数。这个参数也是Parallel Scavenge和ParNew的重要区别。
算法复制算法 应用:适合在后台运算而不需要太多交互的任务
新生代ParNew/Parallel Scavenge与年老代Serial Old搭配垃圾收集过程图: JVM垃圾回收算法和收集器
4. Serial Old收集器 是Serial收集器的老年代版本,也同样是一个单线程的收集器,使用标记-整理算法。主要是client模式下的虚拟机使用。参考上面图Serial/Serial old. 两大用途 (1) JDK1.5及之前的版本中与Parallel Scavenge搭配使用 (2) 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用 收集过程:暂停所有用户线程, 单线程 算法:标记-整理算法 应用:主要意义是Client模式下的收集器,如果在Server模式下:参看上面的两大用途。
5. Parallel Old收集器 是Parallel Scavenge收集器的老年代版本, 在JDK1.6中才开始使用 由于之前的版本中,Parallel Scavenge只有使用Serial Old作为老年代收集器,其吞吐量优先的设计思路不能被很好的贯彻. Parallel Old收集器出现后,Parallel ScavengeParallel Old的配合主要用于贯彻这种吞吐量优先的设计思路 收集过程:多线程 算法:标记-整理算法 应用:在注重吞吐量及CPU资源敏感的场合,可以优先考虑Parallel Scavenge加Parallel Old收集器
新生代Parallel Scavenge和年老代Parallel Old收集器搭配运行过程图: JVM垃圾回收算法和收集器
6. CMS收集器 Concurrent Mark Sweep 以获取最短回收停顿时间为目标的收集器,比较理想的应用场景是B/S架构的服务器。 CMS收集器工作过程: JVM垃圾回收算法和收集器 基于标记-清除算法实现,运行过程分成4个步骤:   a)初始标记(需要stop the world),标记一下GC Roots能直接关联到的对象,速度很快         b)并发标记,进行GC Roots Tracing的过程。   c)重新标记(需要stop the world),为了修正并发标记时用户继续运行而产生的标记变化,停顿时间比初始标记长,远比并发标记短。   d)并发清除 缺点:     1). CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量就会降低。     CMS默认启动的回收线程数为(CPU数量+3)/4。为了解决这一情况,有一个变种i-CMS,但目前并不推荐使用。     2) .CMS收集器无法处理浮动垃圾(floating garbage),浮动垃圾即在并发清除阶段因为是并发执行,还会产生垃圾,这一部分垃圾即为浮动垃圾,要等下次收集。。     同样由于CMS GC阶段用户线程还需要运行,即还需要预留足够的内存空间供用户线程使用,因此CMS收集器需要预留一部分空间提供并发收集时的程序运作使用     默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活这个值可以用-XX:CMSInitiatingOccupancyFraction来设置。     要是CMS运行期间预留的内存无法满足程序需要,就会出现concurrent mode failure,这时候就会启用Serial Old收集器作为备用进行老年代的垃圾收集     3). 空间碎片过多(标记-清除算法的弊端),提供-XX:+UseCMSCompactAtFullCollection参数,应用于在FULL GC后再进行一个碎片整理过程; -XX:CMSFullGCsBeforeCompaction,多少次不压缩的full gc后来一次带压缩的。
7. G1收集器 (jdk1.7后全新的回收器, 用于取代CMS) HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。其与其它收集器相比,G1具备如下特点:
  • 并行与并发:和CMS类似。
  • 分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了
  • 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
    -XX:+UseG1GC:使用 G1 回收器。 G1收集器与前面的CMS收集器相比有两个显著的改进:     一是G1收集器是基于标记-整理算法实现的收集器,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。     二是它可以非常精确地控制停顿即既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。      G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1整个Java堆划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的来由) 区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率     在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set避免全堆扫描     G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作检查Reference引用的对象是否处于不同的Region之间(在分代中例子中就是检查是否老年代中的对象引用了新生代的对象),如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中     当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。 如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
  • 初始标记(Initial Making)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)
    看上去跟CMS收集器的运作过程有几分相似,不过确实也这样。     初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。     并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。     最终标记阶段需要Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行     最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。下图为G1收集器运行示意图: JVM垃圾回收算法和收集器 总结 传统的GC收集器将连续的内存空间划分为新生代老年代永久代JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址是连续的
而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region每个Region占有一块连续的虚拟内存地址