垃圾回收器

在新生代和老年代进行垃圾回收的时候,都是要用垃圾回收器进行的,不同的区域不用同样的垃圾回收器。ParNew CMS. G1常用的三种垃圾回收器

  • Serial和Serial Old 垃圾回收器分别用来回收新生代和老年代垃圾回收对象,工作原理就是但形成运行,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们的系统直接卡死不动,然后让他们垃圾回收。这个现在一般写后台java系统几乎不用
  • ParNew 和 CMS 垃圾回收器:ParNew现在一般都是在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,他们都是多线程并发的机制,性能更好
  • G1 垃圾回收器:为取代CMS而生,采用了更优秀的算法和设计机制。将Java堆内存拆分成大小一致Region。
  • CMS:CMS(并发标记收集器):Conçurrent Mark Sweep,使用了一个或多个垃圾收集器线程,这些垃圾收集器线程与应用程序同时运行,CMS收集器在应用程序仍在运行的情况下,执行大部分跟踪和清除工作,因此应用程序仅会短暂的暂停

垃圾回收线程、垃圾回收器、垃圾回收算法

比如针对新生代我们会用到ParNew 垃圾回收器来进行回收,然后ParNew垃圾回收器针对新生代采用的就是复制算法来进行的垃圾回收,这个时候垃圾回收器就会把Eden区域中的存活对象标记出来,然后全部转到Survivor1去,接着一次性清空掉Eden中的垃圾对象。接着系统继续运行,新的对象继续分配在Eden中。当Eden再次再次塞满的时候就要出发Minor GC了,此时依然是垃圾回收线程运行垃圾回收器中算法的逻辑,也是采用复制算法逻辑,去标记出来Eden和Survivor1中存活的对象。然后一次性的把存活对象转移到Survivor2中去,接着吧Eden和Survivor1回收掉
image.png

Garbage Collector还能继续创建新对象吗

如果一边垃圾回收一边想办法把Eden和Survivor2里面的存货动向标记出来,转到Survivor去,然后再想办法把Eden和Survivor2里面的垃圾对象都清理掉,结果这个时候程序系统还在不停的在Eden里创建新的对象。
这些对象有的很快的成了新的垃圾对象,有的还有人引用。

JVM 的痛点(Stop the World)

平时使用的JVM最大的痛点,其实就是在垃圾回收的过程中。因为在垃圾回收的时候,尽可能要让垃圾回收器专心致志的干工作。不能随便让我们写的java系统继续创建对象了,所以此时的jvm会在后台直接进入 stop the world 状态,让垃圾回收线程可以专心致志的工作。

假设我们的Minor GC要运行100ms,那么就可能导致我们的系统直接停顿100ms不能处理任何请求。在这100ms期间用户的发起的所有请求都会出现短暂的卡顿,因为系统的工作线程不能运行,不能处理亲求。

image.png

不同垃圾回收器的不同影响

比如对新生代的回收,Serial垃圾回收器就是用一个线程进行垃圾回收,然后此时暂停系统工作线程,所以我们在服务中恒少用这种方式。但是我们平常用的新生代垃圾回收器ParNew, 他针对的一般都是多核cpu做了优化,他是支持多个线程的垃圾回收,可以大幅度的提升回收的性能,缩短回收的时间。大致原理如图:
image.png

最常用的新生代垃圾回收器:ParNew

一般来说在之前的多年里面,假设没有最新的G1垃圾回收器的话,通常大家线上系统都是ParNew垃圾回收器垃圾回收器作为新生代的垃圾回收器。
新生代ParNew垃圾回收器主打的就是多线程的垃圾回收机制,另外一种Serial垃圾回收器主打的是单线程垃圾回收,他们两都是回收新生代的,唯一的区别就是单线程和多线程的区别,但是垃圾回收算法完全一样。
ParNew垃圾回收器如果一旦在合适的时机执行MinorGC的时候,就会把系统程序的工作线程全部停掉,禁止程序继续运行创建新的对象,然后就自己用多个垃圾回收线程去进行垃圾回收。

image.png

如何为线上系统制定ParNew垃圾回收器

一般来说,对于线上系统部署的时候,设置jvm参数,在idea中可以设置debug jvm arguments 使用java -jar 命令启东市在后面跟上jvm参数,不属骚tomcat是可以在tomcat中的catalina.sh中设置tomcat jvm参数。ParNew垃圾回收器默认情况数量 = cpu核心数(默认)
-XX:+UseParNewGC
-XX:ParallelGCThreads
-XX:+UseG1GC

CMS垃圾回收器的基本原理

(标记清理算法)一般老年代我们选择的垃圾回收器是CMS,它采用的是标记清理算法,标记方法去标记处哪些对象是垃圾对象,然后把这些垃圾对象清理掉。
当老年代内存空间小于(lt)历次Minor GC后升入老年代对象的平均大小,判断MinorGC有风险,可能就会提前触发Full GC回收老年代的垃圾对象。或者一次Minor GC 后对象太多了,都要升入老年代没发现空间不足,触发一次Full GC,标记清理算法,先通过追踪GC Roots的方法,看看各个对象是否被GC Roots给引用了。如果是就是存活对象,否则就是垃圾对象。
假设要先 stop the world 然后再采用 标记清理算法,会有什么问题呢
答:如果停止一切线程的工作,然后慢慢去执行标记清理算法,会导致系统卡丝时间过长,很多响应无法处理。所以CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时去执行的模式来处理

CMS如何实现系统一边工作一边进行垃圾回收

cms在执行一次垃圾回收的过程中分为4个阶段

  • 初始标记:cms要进行垃圾回收时,会先执行初始标记阶段,这个阶段会让系统的工作线程全部停止,进入stop the world状态,所谓初始标记,他是说标记出来的所有GC Roots直接引用的对象。虽然要暂停stop the world 但其实影响不大,速度很快。
  • 并发标记:这个阶段会让系统可以随意创建各种新对象,继续运行。在运行期间可能会创建新的存活对象,也会让部分存活对象失去引用,变成垃圾对象,在这个过程中,垃圾回收线程会尽可能的对已有的对象进行GC Roots追踪。(对老年代所有对象进行GC Roots追踪,其实是耗时的)
  • 重新标记:在第二阶段里,一遍标记存活对象和垃圾对象,一遍系统在不停运行创建对象,让老对象变成垃圾对象,第三阶段会继续让系统程序停下来,再次进入stop the world阶段,标记第二阶段新创建的一些对象,还有一些已有对象可能失去引用变成垃圾的情况。(对于在第二阶段中被系统运行变动的少数对象进行标记,速度快)
  • 并发清理:这个阶很耗时,需要进行对象的清理名单他也是根系程序编发运行的,所以不影系统程序的执行。

CMS的垃圾回收机制进行性能分析

cms的垃圾回收机制,就会发现在性能优化上已经做的很好了,因为最好是的其实就是对老年代对象进行GC Roots追踪,标记哪些对象可回收,然后就是对各种垃圾对象从内存里清理掉,比较耗时。

CMS Garbage Collector

  • cms并发回收垃圾导致cpu资源紧张:

    cms垃圾回收器有一个最大的问题,虽然能在垃圾回收的同时让系统同时工作,但在并发标记和并发清理两个最耗时的阶段,垃圾回收线程和系统工作线程同时工作,会导致有限的cpu资源被垃圾回收线程占用一部分。并发标记的时候需要对GC Roots进行深度追踪,看到所有对象里面到底有多少人是存活的。但因为老年代里存活的对象比较多,这个过程会追踪大量的对象,所以耗时比较高,并发清理,有需要把垃圾对象从各种随机的内存位置清理掉,也是比较耗时。cms默认启动的垃圾回收线程是(cpu+3)/4

  • Concurrent Mode Failure问题

    在并发清理阶段,cms Garbage Collector 只不过是回收执勤标记好的垃圾对象,但这个系统一直运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是浮动垃圾。cms Garbage Collector 触发时机,其中有当老年代内存占用达到一定比例了,就自动执行gc.”-XX:CMSSinitiatingOccupancyFaction” 参数可以用来设置老年代占用多少比例的时候触发垃圾回收,jdk1.6默认的是92%,cms垃圾回收期间,系统程序要放入老年代的对象大于可用对象,就会发生Concurrent Mode FAilure,就是说并发垃圾回收失败了,此时就会使用Serial Old 垃圾回收器替代cms,就直接呛醒吧系统 stop the world 重新进行长时间的GC Roots 追踪,标记出来的全是垃圾对象,不会产生新的对象。因此在生产中需要进行合理优化,避免产生Concurrent Model Failure 问题

  • 内存碎片的问题

    老年代的CMS采用 标记-清理算法,每次都是标记出来垃圾对象,然后一次性回收掉,这样会导致大量的内存碎片产生,如果内存碎片太多,就会导致后续对象进入老年代找不到可以连续内存空间了,然后触发full Gardage Collector。CMS还有一个参数是 -XX:+UseCMSCompactAtFullCollection 默认就是打开的,意思是在Full GC 之后要在此进行Stop the world,停止工作线程,让后进行碎片整理,把活的对象移动到一起,空出连续内存空间,避免内存碎片。-XX:CMSFullGCsBeforeCompaction,意思是执行多少次Full GC之后在执行一次内存碎片的整理工作,默认是0 每一次Full gc之后都会整理一次内存空间

为什么Full gc 比Minor gc 慢很多

新生代执行速度其实很快,因为直接从GC Roots出发就追踪到了哪些对象是活动,新生代存活对象比较少,速度极快,不需要追踪多少对象。然后直接把存活对象放入Survivor中,就一次性回收Eden 和 之前使用的Survivor,但是cms的full gc,他在并发标记阶段,他需要追踪所有存活对象,老年代存活对象很多,这个过程就会很慢。其次并发清理阶段也不是一次性回收一大片内存,而是零零散散的在各个地方的垃圾对象,速度也很慢。最后完事了,还需要进行一次碎片整理,把大量的活动对象挪到一起,空出来连续内存空间,还需要stop the world。

为什么新生代用的是复制算法,而老年代用的标记整理算法,既然复制算法比较快,为什么老年代不采用新生代的复制算法呢?
答:因为老年代的存活对象太多了,采用复制算法来来会移动大量的对象,效率会更差。

触发老年代的时机

  1. 老年代可用内存lt(小于)新生代全部对象的大小,如果没有开启空间担保(HandlePromotionFailure),就会就会触发Full GC ,所以空间担保一般会被打开。
  2. 老年代可用内存lt(小于)历次新生代GC后进入老年代平均对象大小,此时会提前进入GC
  3. 新生代Minor GC后的存活对象大于Ssurvivor,那么就会进入老年代,此时老年代内存不足。
  4. -XX:CMSlnitiatingOccupancyFaction参数,表示老年代空间使用多少后出发full gc

新生代与老年代

  • 老年代与新生代回收都需要“stop the world”,因为必须要让程序停止创建对象,才能回收垃圾对象。Full GC 和 Major GC都是指的老年代的GC,只不过一般会带着一次Minor GC,也就是Yong GC, 他们是一个概念多种名词。
  • jvm的调整,需要更具堆对象的生命周期特征,合理分配各区域大小,让对象在新生代被回收,避免进入老年代,造成过多的full GC。开启内存担保机制会减少full gc。

如何避免Full GC

  1. 保证老年代的可用空间大雨新生代所有对象,避免Minor GC前进行Full GC,
  2. 如果1可以保证,那么-XX:HandlePromotionFailur, 进入老年代的平均大小就不需要考虑了。
  3. 保证Minor GC 后存活对象不大于 Survivor


如何避免新生代进入老年代

  • 根据实际情况(每次Minor GC后存活对象的大小)设置合适Eden 和 Survivor区域,保证存活对象进入Survivor区域而不是老年代。
  • 根据对象的存活时间以及Minor GC的时间间隔,确定年龄,比如三分钟一次的Minor GC, 而对象可以存活1个小时,那就把对象设置成20,避免对象15岁进入老年代。
  • 大对象如果偶尔创建一个,可以设置-XX:PretenureSizeThreshold,使其分配至年轻代,如果创建销毁频繁,就让其直接进入老年代,利用对象池避频繁创建和销毁。

ParNew + CMS 的组合痛点:

Stop the World 无论是新生代垃圾回收,还是老年代垃圾回收,都会产生Stop the World 现象,所以之后的垃圾回收器的优化都是朝着减少Stop the World 目标去做。

回顾思考

  1. 对象在新生代的分配
  2. 什么时候回触发minorGC
  3. 触发Minor GC 之前会如何检查老年代可用内存大小和新生代对象大小。
  4. 如何检查老年代可用内存大小和历次minor GC 之后升入老年代的平均对象
  5. 什么情况下Minor GC 之前会提前触发Full gc
  6. 什么情况下会直接触发minor GC
  7. Minor GC 之后有哪几种情况对象会进入老年代。