HotSpot如何回收垃圾
HotSpot作为JVM中的大太子,对垃圾回收必定很有心得。今天咱们就来聊聊它是怎么处理垃圾的。
垃圾分类
上回咱们说过,JVM中的垃圾,按照存活时间不同,分为新生代和老年代。和垃圾分类原理一样,不同种类的垃圾,要用不同的垃圾收集器来回收。
新生代垃圾收集器
针对新生代垃圾,主要包含三种垃圾收集器:Serial垃圾收集器,ParNew垃圾收集器,Parallel Scanvenge垃圾收集器。
Serial垃圾收集器:这种收集器采用单线程执行,当垃圾收集开始,就停止一切用户线程,这个操作非常凶猛,人称“Stop The World”。这种收集器适合客户端,因为需要单线程进行GC遍历,客户端对象一般都比较少,所以很适合使用这种方式。
ParNew垃圾收集器:工作机制和Serial没有任何区别。唯一的区别就是采用多线程的遍历方式,如果是多个CPU,这么做效率会提升。单个CPU,线程来回切来切去反而损耗了垃圾回收的性能。
Parellel Scanvenge垃圾收集器:这个收集器和上面的ParNew一样都采用了多线程的收集方式。不同点在于:ParNew追求降低用户的停顿时间,就是让用户尽量感受不到垃圾回收的耗时;而这种方式追求的是个新东西:CPU吞吐量。这是啥玩意?有个公式:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。从公式上可以看出,提高吞吐量,运行用户代码时间是不好改变的,但可以缩短垃圾收集时间。这个垃圾收集时间,表示的是整个的垃圾收集时间,比如一共进行了五次收集,这个垃圾收集时间就是这五次时间的总和。所以,就带来一个问题,如果通过降低GC次数来降低整个GC时间,次数降低导致每次需要回收的垃圾个数增加,最终导致整体垃圾时间上升;如果通过频繁运行GC降低单次GC回收垃圾的个数,整体的回收垃圾时间又会升高。所以,如果采用这个收集器,需要考虑:
1.参数GCTimeRadio:设置GC时间占总CPU时间的百分比;
2.参数MaxGCPauseMills:设置垃圾处理过程最久停顿时间;
3.参数UseAdaptiveSizePolicy:开启自适应策略。设置好堆大小和上面两个参数,JVM会通过自适应策略来调整新生代大小、Eden和Survior比例、对象进入老年代年龄,最终实现最大程度的接近我们设置的两个参数标准。
老年代垃圾收集器
说完年轻垃圾,来说说老年垃圾。和新生代垃圾收集类似,老年代也分为以下几种:
Serial Old垃圾收集器:就是Serial的老年版。也是单线程,也是遍历。唯一的区别:新年代采用的是复制算法,老年代采用标记整理算法。为啥?因为新年代对象生命周期短,不像老年代,实在太能活了,如果每次都复制,效率太低。所以采用排序整理,根据地址排序,地址范围以外的全部当成垃圾回收就OK,提高效率。
Parellel Old垃圾收集器:就是ParNew的老年版。
CMS垃圾收集器:这个收集器将低停顿做到了极致,方法就是将用户线程和GC线程同步执行,用户使用的时候,几乎感受不到卡顿。具体的执行流程如下:
1.初始标记:使用一个GC线程,对所有GC Roots直接关联的对象标记;
2.并发标记:使用多个标记线程,和用户线程并发执行,进行可达性分析,标记出所有的废弃对象;这步比较耗时;
3.重新标记:由于上一步用户线程还在执行,仍会产生废弃对象,所以在这步首先Stop The World,停掉全部用户线程,然后启动多个标记线程,把上次新出现的废弃对象标记出来;
4.并发清除:用一个GC线程,和用户线程并发执行,将之前所有的废弃对象全部清除。显而易见,这步最为耗时。
从上面可以看出,CMS有吞吐量低,无法处理浮动垃圾导致频繁full GC;使用标记清楚算法,会产生碎片。针对碎片问题,通过开启-XX:+UseCMSCompactAtFullCollection,每次full GC完成后,会进行一次零散内存的压缩整理,把碎片压缩到一块儿。-XX:CMSFullGCsBeforeCompaction这个参数的设置会告诉JVM,完成多少次full GC后,进行一次压缩整理。
G1通用垃圾收集器
最后一种。上面两种按代数分的收集器,那么,有没有一种不分新生代老年代的垃圾收集装置呢?有!它来了,就是G1通用垃圾收集器。
这种收集器,不分什么新生代老年代,就是分成一个个Region,其实就是一块块区域,完了估摸下每个区域大概能回收出多少垃圾,可着油水最多的先回收。总体上来说,它用的是标记-整理算法,对于没两个Region而言,它又是复制算法的使用。所以,这两种算法都不会导致碎片的产生。它的执行步骤如下:
1.初始标记:上来就Stop The World,完了用一个线程标记所有GC Roots直接关联的对象;
2.并发标记:用一个标记线程和用户线程并发执行,进行可达性分析;
3.最终标记:Stop The World,用多个线程并发标记;
4.筛选回收:还是Stop The World,完了启动多个线程回收废弃对象。
需要注意个问题:如果一个对象A,出现在Region一半,出现在Region另一半,那垃圾回收的时候,那道需要对所有堆内存来一遍可达性分析?不是,每个Region都有个Remembered Set来记录本区域中所有引用对象所在的区域,可达性分析时候用这个记录就能顺利找到对应的对象进行标记了。避免了遍历所有内存的浪费。
12点了。总算写完了。下回聊内存分配与回收的策略。今天是我31岁的生日,25岁参加工作,如今也有五年的时间了。一直做C++开发,现在想转Java,这条路注定会非常艰辛。但不管多难,是否真的是正确的,但实现这个目标,对我一定有非常大的意义。加油!奥利给!上诗!
三十一载人生路,白驹过隙如昨天;转型艰难多凶险,但求追梦勇向前!