经典垃圾收集器介绍
垃圾收集器是实现内存回收的工具;
垃圾回收器没有完美一说,要针对不同的场景和任务选择最优的垃圾回收器组合;
垃圾回收器作用于不同的分代,新生代:Serial、ParNew、Parallel Scavenge,老年代:CMS、Serial Old、Parallel Old。G1可以作用于新生代和老年代。
1、Serial收集器
最基础的垃圾收集器;
单线程垃圾收集器;
使用的是标记-复制算法作用于新生代;
在其进行GC的时候其他的进程就会停止:stop the world;
Serial收集器是额外内存消耗最小的收集器,因为在其执行过程中终止其他进程,使其执行效率很高;
额外内存消耗是指在GC过程中由于GC的发生使用的那部分内存;
通常在内存资源占用小的场景中(例如用户桌面应用以及微服务,类似场景给虚拟机分配的内存很小),Serial收集器非常适用,因为其单线程回收效率最高。而且这种场景下,就算会暂停用户线程也就很短时间的事情,不会影响用户的体验。
2、 ParNew收集器
ParNew收集器是Serial收集器的并行版本,可以支持多个GC进程同时开始,其他的地方与Serial收集器没有不同;
其优势是可以和CMS组合使用;
在单核处理器以及伪双核处理器的情况下,Serial收集器的效率要高于ParNew收集器,因为Serial收集器没有并行交互开销;
在多核处理器的情况下,ParNew的并行收集效率更高,通常有几个核就开几条并行处理的线程。
并行:是指多条垃圾收集器的线程同时进行,此时用户线程处于等待状态。
并发:是指垃圾收集器的线程和用户线程同时进行。
3、 Parallel Scavenge收集器
Parallel Scavenge收集器也是一个多线程并行收集器,和ParNew相似,不同点在于,他关注的是一个特定的吞吐量,而不是减少GC过程中用户的等待时间;
吞吐量=运行用户代码的时间/运行用户代码的时间+GC时间,吞吐量越高意味着在整个执行过程中,GC占的时间短,更高效的利用处理器的资源。
两个代码来控制:
-XX:MaxGCPauseMillis:垃圾回收最大的等待时间。这个是通过牺牲吞吐量来实现的,所以并不是设置的越小越好,设计的小的话GC的数量会增加,总GC时间也会增加。
-XX:GCTimeRatio:设置1-99,设置的越高,吞吐量越大。
Parallel Scavenge收集器存在自适应调节策略,通过函数:-XX:+UseAdaptiveSizePolicy,来开关,使用这个策略之后,我们就不需要指定新生代各个区以及晋升老年代的大小,只需要设置分配给虚拟机的总内存数,和上面那两个参数,虚拟机就会根据当前系统的情况去动态调整这些信息。这是区别于ParNew的一个重要特征。
4、 Serial Old收集器
Serial收集器的老年代版本,使用的是标记-整理算法;
是一个单线程收集器;
可以和parallel scavenge收集器搭配使用;
作为CMS失败后的预备方案使用;
5、 Parallel Old收集器
Parallel scavenge收集器的老年代版本,使用标记-整理算法;
在对吞吐量有要求或者处理器资源有限的情况下可以使用parallel scavenge+parallel old(吞吐量优先收集器的搭配);
6、 CMS收集器
CMS收集器使用的是标记-清除算法;
目标是减少GC期间用户等待的时间;
CMS收集器有四个过程:初始标记-并发标记-重新标记-并发清除;初始标记非常快,此时用户线程是等待的stop the world,目标是标记GCroots能关联到的对象;并发标记时间最长,可以和用户线程同时进行,这个过程中遍历了整个对象图;重新标记也很快,他是为了修正在并发标记过程中发生变化的对象的标记,这个过程中,用户线程是等待的;最后是并发清除,清除掉需要清除的对象。
由于时间最长的并发标记和并发清除阶段都是和用户线程并行的,所以CMS收集器整体上是并发收集;
GMS收集器有三个缺点:
1、GMS收集器对处理器的性能要求很高,通常需要4个以上的核。因为在并发阶段GC进程和用户进程共用处理器的资源,这样会让用户感觉到用户进程的速度一下子降低了。
2、GMS收集器存在浮动垃圾,原因是在并发标记阶段用户进程还在执行,会产生新的垃圾,因为此时并发标记已经结束了,这部分新的垃圾将放到下次GC进行处理,但如果内存小于这部分新的垃圾的大小,没有留出足够的空间,就会发生并发失败,此时会用Serial Old来进行老年代的垃圾收集;
-XX:CMSInitiatingOccu-pancyFraction的作用是:设置一个值,当老年代内存达到了这个值就会自动启动CMS收集器,所以这个参数设置较小的话会导致GC的过程过于频繁,但如果设置的值过大的话会导致并发失败
3、GMS收集器因为是标记-清除算法,会有空间碎片的存在,导致看上去内存空间剩的很多,但是放不下大内存的对象,这种情况下就会触发fullGC。下面两种方法来解决
-XX:+UseCMS-CompactAtFullCollection 在每次要进行FullGC的时候进行一次内存碎片整理。
-XX:CMSFullGCsBeforeCompaction 在执行了n次的fullGC后下次进入fullGC之前进行一次内存碎片整理
7、 G1收集器
G1收集器是取消了固定的新生代老年代的划分,把整个java堆划分成若干个等大的region。
G1收集器能够建立停顿时间模型的原因:是因为收集器可以根据设定好的停顿时间去选择性的收集这些region,收集每个region之前,收集器会根据这个region历史的收集时间去估算这个region需要多少时间能收集,然后再计算这些region的回收价值也就是回收后能得到的空间,最后选择总共回收时间不超过设定好的停顿时间的若干个region去回收。
大对象分配在一个叫Humongous的区域,大小超过region一般的对象就叫大对象。-XX:G1HeapRegionSize 来设置region大小,通常把Humongous当作老年代来看待。超过一个region那么大的对象放在N个连续的humongous区域
因为每个region大小有限,有些连续的大内存要占好几个region,如何解决这个问题:G1为每个region建立了一个记忆集,这个记忆集是一个哈希表,key是有哪些region指向了这个region,value是卡表的引用信息。卡表的意思是:每个region区域分成了几个卡表,卡表会记录这个卡片是否存在跨区引用等信息,通过这种哈希表加卡表的结构,来解决这个跨区引用的问题,可以避免做全局扫描。但坏处就是region需要有额外的空间去存储记忆集。
因为G1要实现并发运行,在用户线程执行期间,有些对象的引用可能会发生改变,但是需要保证并发标记的对象图还没有改变,这需要使用SATB(原始快照)算法来实现,就是在并发标记开始的时候把所有存活的对象记录下来。
并发标记阶段新对象的创建是使用了叫TAMS的指针,这指针把region分为了两部分,上半部为可以添加新对象的区域,下部分是之前存在的老对象,通过SATB来判断,回收的时候会默认上半部分的新区域的对象为默认标记过的,不会在这次对其进行回收。
G1收集器分为四个阶段:1、初始标记,这个阶段是要标记能够被GC roots直接关联到的对象,同时用TAMS把region区分好,划分出空间给下个阶段的新对象分配,在这个阶段用户线程是停止的。2、并发标记:在这个阶段用户线程和标记线程同时进行,扫描递归整个对象图,找到要回收的对象,耗时较长,同时还要处理在SATB中引用有变动的对象。3、最终标记:处理少量SATB的记录。4、筛选回收:根据各个region的时间和价值来回收。回收过程是把标记的那部分对象复制到新的region上,然后清理整个旧的region的空间。
整体使用的是标记-整理算法,局部例如卡表那里用的是标记-复制算法,所以不会产生空间碎片。
不足在于,因为卡表更复杂,已经需要记忆集等原因,他需要占用的额外内存要更多,所以小内存的情况应该用CMS,6-8GB以上用G1
G1需要写前屏障,用来实现SATB操作,也需要写后屏障来维护卡表,而CMS只需要写后屏障,来维护卡表,而且G1的卡表比CMS复杂的多,所以要消耗更多的运算资源。
G1的垃圾回收遵循的不是一下子都清理完,他是要让收集器内存回收速度不低于分配器分配速度就行,达到一个动态平衡。
选择合适的垃圾收集器的小方法
1、 何时使用Epsilon收集器
Epsilon收集器不能进行垃圾回收,只需要完成一些像堆内存管理对象分配这些工作,这是因为对于一些时间短、规模小的服务来说,例如微服务和无服务,虚拟机正常分配内存,在堆满之前退出服务即可,就不需要垃圾的回收。
2、 其他收集器的权衡
首先要考虑应用的关注点是什么,比如说是数据分析等需求,就需要更快的得出结果,就需要关注垃圾收集器的吞吐量;如果是SLA应用,就需要关注服务的质量,需要减少等待的时间,如果是客户端应用或者是嵌入端应用,需要关注垃圾占的内存大小。
还要考虑处理器的个数、内存分配的大小、操作系统是什么、系统架构是什么、所用的JDK的发行商和版本号是什么。
如果预算充足没有什么调优经验,使用C4
如果能掌握硬件版本,使用较新的版本,又在意延迟,用ZGC
如果要提高实验的稳定性,或者必须用在windows系统,用shenandoah
如果版本很老,软硬件设施落后,那么考虑内存,6-8GB以上用G1,6-8GB以下用CMS。