一、垃圾回收
垃圾回收一般发生在堆或方法区中,也就是线程共享的部分,堆和方法区的内存分配和垃圾回收都是通过垃圾回收器去实现的。
不同的垃圾回收器对应不同的垃圾回收算法。
(一)判断算法
1、判断哪些对象需要回收
判断哪些对象需要回收主要有引用计数法和根搜索算法。
引用计数法:给对象添加一个引用计数器,每一次被引用该计数器就会加一,当引用失效时,该计数器就会减一,当计数器的值为0时,就可以进行垃圾回收。其优点就是实现简单、判断效率高;缺点是很难解决循环引用的问题,这也是java语言没有使用该算法的原因。
根搜索算法:也叫做可达性算法,通过GCRoot作为根节点开始向下搜索,搜索走过的路程称为引用链,当一个对象到GCRoot没有任何一条引用链时,则证明该对象没有被使用。
可以作为GCRoot的对象:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象,总结就是堆中的对象不能作为GCRoot
2、垃圾回收过程
可达性分析后如果对象是不可达的,也并非直接就进行回收,这只是第一次标记,而对象进行回收需要进行两次标记,其中第一次标记就是前面说的使用GCRoot进行搜索后没有引用链的对象进行标记,第二次标记是在第一次标记后紧接着会有一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,在finalize()方法zhong没有重新与GCRoot建立联系的对象,将会被第二次标记,
第二次标记成功的对象就会被真正的回收,如果在调用finalize方法时,对象与GCRoot重新建立了联系,那么该对象就会逃离本次回收。
3、方法区回收
方法去也是有垃圾回收的,主要回收废弃的常量和无用的类,由于回收性价比极低,因此就算满足回收条件也不一定会回收。
废弃常量:例如字符串常量池,没有对象引用即可回收,同时常量池中类、方法、字段的符号引用也与此类似。
无用的类:需要满足三个条件:(1)该类的所有实例都已经被收回;(2)该类的ClassLoader已经被收回;(3)该类对应的Class对象不存在任何的引用,不能通过反射产生该类的实例对象
(二)回收算法
垃圾回收算法有标记清除算法、复制回收算法、标记整理算法、分代回收算法等。
1、标记清除算法
标记清除算法是最基本的算法,分为标记和清楚两个阶段,首先标记出需要清理的对象,在标记完成后,统一回收掉被标记的对象。
缺点是效率低(标记和清楚效率都不高)及存在内存碎片(会产生大量不连续的内存碎片,导致大对象不能存储,提前触发GC)。
2、复制回收算法
为了提高效率,其将内存分为两块大小相等的区域,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上,然后再把已使用的内存清除掉。
现代商业虚拟机都是使用复制回收算法来对新生代进行垃圾回收的,每次回收时会将Enden区和其中一个S区中存货的对象复制到另一个S区,然后清空Ende区和该S区,在Hotspot中,Eden区和S区的比例为8:2,S0:S1为1:1,因此在新生代中使用的:未使用的比例为9:1,浪费了十分之一的内存,但是却大大提高了效率。
当S区内存不够使用时,则会依赖老年代进行内存担保。
3、标记整理算法
标记过程仍然使用标记清除算法,在清除完毕后,将还存活的对象都向一端移动,然后清理掉边界以外的内存。
老年代没有区域对其进行内存担保,因此其使用标记整理算法。
4、分代回收算法
目前现在的商用虚拟机都是采用这种算法,跟队对象的存活周期将内存划分为几个不同的区域,然后根据每个区域的特点进行不同的垃圾回收。
新生代:由于每次回收都有大量对象被回收,因此使用复制回收算法。
老年代:对象存活率高,无其他区域对其进行内存担保,就必须采用标记清除和标记整理算法。
(三)内存担保机制
内存担保是在内存分配时,新生代内存不足时,把新生代存活的对象搬到老年代,然后将腾出来的内存用于存放最新的对象。
在不同的GC机制下,担保机制也略有不同,在Serial+Serial Old组合下,发现存不下就直接启动内存担保,而在Parallel scavenge+Serial Old组合下, 先要看下要分配的内存是都大于Eden区的一半,如果大于,就直接把对象放在老年代,如果不大于,才会开启内存担保。
(四)GC方式
GC方式分为minorGC、majorGC和fullGC三种。
minorGC:新生代的垃圾回收,很快就回收了,使用的是一些垃圾回收比较快的算法,例如复制回收算法。
majorGC:老年代的垃圾回收,比minorGC慢10倍,因此要尽量避免majorGC。majorGC不是fuuGC,majorGC只针对堆,而fuuGC不但针对堆,还针对方法区。
fuuGC:整个堆和方法区的垃圾回收;老年代不够用,没有其他内存区域对其进行内存担保,会触发fuuGC;方法区不够用,没有其他内存区域对其进行担保,会触发fuuGC;当新生代无法被老年代成功担保时,也会发生fuuGC
二、垃圾收集器
(一)垃圾回收器概述
为了达到最大性能,基于分代管理和回收算法,并结合回收的时机,JVM实现的垃圾回收器有:串行回收、并行回收、并发标记回收(CMS)和垃圾优先回收(G1)。
1、串行回收
串行回收使用单线程进行垃圾回收,在回收时应用程序需要STW。
新生代通常使用复制算法,老年代通常使用标记压缩算法。
2、并行回收
并行是指多条垃圾回收线程并行工作,但此时用户线程仍然处于等待状态。并发是指用户线程和垃圾回收线程同时执行,有可能用户程序运行在一个cpu上,垃圾回收运行在一个cpu上,也有可能用户程序和垃圾回收在同一个cpu上交替执行。
并行回收也需要SWT。
新生代使用复制算法,老年代使用标记压缩算法。
3、并发标记回收
并发标记回收(CMS)的整个回收期划分为多个阶段:初始标记、并发标记、重新标记、并发清除等。
在初始标记和重新标记阶段需要SWT,在并发标记和并发清除阶段可以和应用程序一起执行,这个算法通常用于老年代,新生代可以使用并发回收。
4、垃圾优先回收
连续的内存将导致垃圾回收时间过长,停顿时间不可控,因此G1将堆拆分为一系列的分区,这样在一段时间内,大部分的垃圾收集器都只针对一部分分区,而不是整个堆或整个老年代。
(二)垃圾回收器
垃圾收集器是垃圾回收算法的具体实现(标记清除、复制回收、标记整理、分代收集),不同的虚拟机提供的垃圾回收会有很大差别,Hotspot虚拟机中的8种垃圾回收器:Serial、parNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1、ZGC
1、Serial收集器(新生代)
收集区域:新生代
收集方式:串行
收集算法:复制算法
串行收集器的组合:Serial+SerialOld
优点:简单有效;对于单CPU环境,Serial不需要线程切换的开销,可以获得高效的收集效率;在应用桌面的程序中,可用内存一般不大,可以在短时间内完成垃圾收集。
应用场景:主要用于client模式、单CPU、桌面应用程序。
参数配置:-XX:UseSerialGC
2、Serial Old收集器(老年代)
收集区域:老年代
收集方式:串行
收集算法:标记整理算法
应用场景:主要用于client模式
3、ParNew收集器(新生代)
收集区域:新生代
收集方式:并行方式
收集算法:复制算法
应用场景:在Server‘模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有他能与CMS收集器配合工作;ParNew是Serial收集器的多线程版本。
4、Paralell Scavenge收集器(新生代)
收集区域:新生代
收集方式:并行方式
收集算法:复制算法
因为与吞吐量关系密切,也被称为吞吐量收集器,并行收集器的组合:Paralell Scavenge + Serial Old,并行收集器是以关注吞吐量为目标的垃圾收集器,也是server模式下的默认收集器配置。
特点:他的关注点与其他收集器不同,CMS等收集器关注的是用户线程的停顿时间,而Paralell Scavenge关注的目标是达到一个可控的吞吐量。
应用场景:提高吞吐量为目标,减少垃圾收集时间,让用户代码获得更长的执行时间。适用于多CPU,对于停顿时间没有特别要求的后台任务。
5、Paralell Old收集器(老年代)
收集区域:老年代
收集方式:并行方式
回收算法:标记整理
收集组合:Paralell Scavenge + Paralell Old,Paralell Old是Paralell Scavenge的老年代版本,
应用场景:在server模式下、多CPU的情况下
6、CMS收集器(老年代)
收集区域:老年代
收集方式:并行方式
收集算法:标记清除
优点:并发收集、低停顿
缺点:
(1)CMS收集器对CPU资源非常敏感,在并发收集阶段,其虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序运行变慢,从而导致吞吐量下降。
(2)CMS无法处理浮动垃圾,可能会导致并发模式故障从而触发full GC(浮动垃圾:由于CMS的并发标记是和应用程序并发执行的,因此会存在需要被收集但是没有标记到的对象,这些对象就只能在下一次GC中被回收,这些对象就称为浮动垃圾)
(3)CMS使用的收集算法是标记清除,因此可能会存在大量的内存碎片,从而导致full GC
应用场景:与用户交互较多的场景,希望系统停顿时间短,注重服务的响应时间。
参数配置:
-XX:+UseCMSCompactAtFullCollection 强制进行空间碎片整理;CMS 采用标记算法,会产生大量的空间碎片。以上参数就是强制执行一次空间碎片整理,但是空间碎片整理会引发STW。 -XX:+CMSFullGCsBeforeCompaction 配置经过几次的FullGC进行空间碎片整理 -XX:+CMSFullGCsBeforeCompaction=10 经过10次FGC后进行空间碎片整理,以降低STW次数
7、G1收集器
收集区域:整个堆
收集方式:并行方式
收集算法:标记清楚和标记整理。G1收集器并没有在物理内存中使用分代划分,其在物理内存中划分了2048个分区(Region,可以通过-XX:G1HeapRegionSize进行设置),同时其还保留了分代的逻辑。
JDK7和JDK8默认使用ParalellGC进行垃圾回收,JDK9默认使用G1收集器,如果在JDK7和JDK8开启G1收集器,使用:-XX:+UseG1GC
G1不会等到内存耗尽或者快要耗尽时才开始进行垃圾回收,而是在内部采用了启发式算法,在老年代找出具有高收集效益的区进行收集。
G1里面有一个card卡片的概念。G1将堆内存划分为多个内存大小为512Byte的区域,一个区域称为一个card,分配对象会占用物理上连续的几个card;CardTable是一个字节数组,维护者所有的card,card中对象的引用发生变更时,card在cardTable中的值就会被标记为dirty,就称这个card被脏化了。
G1收集器并没有在物理内存中使用分代划分,其在物理内存中划分了2048个分区(Region,可以通过-XX:G1HeapRegionSize进行设置),同时其还保留了分代的逻辑,但是年轻代和老年代不再是物理上的隔离,他们是一部分Region的集合,这些Region可以是不连续的,每个Region可能随着G1的运行在不同代之间切换。 这些Region分区被分为Eden Region、Survivor Region、Old Region、Humongous Region(巨型分区,当一个对象的大小大于一个Region大小的50%时,他就会独占一个或多个Region,巨型对象会直接分配在老年代,该对象所占用的连续分区被称为巨型分区)。
G1的年轻代并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲Region加入到年轻代空间,整个年轻代的空间会在初始空间(-XX:G1NewSizePercent,默认为堆空间大小的5%)和最大空间(-XX:G1MaxNewSizePercent,默认为堆空间的60%)之间动态变化,且有参数目标暂停时间(-XX:MaxGCpauselMillis,默认为200毫秒)、需要扩容的大小及分区的已记忆集合计算得到,当然,G1可以直接设置年轻代的大小(-XX:NewRatio、-XX:Xmn),如果直接设置年轻代大小,则暂停目标将失去意义。
G1 Young GC
Young GC主要对Eden区进行垃圾回收,其会在Eden区空间耗尽的情况下触发,这种情况下,Eden空间存活的对象就会被移动到Survivor空间中,如果Survivo空间的空间不足,则这些对象就会被直接晋升到老年代;Survivor空间的对象会被移动到新的Survivor中,也有部分会晋升到老年代,最终Eden空间的数据为空,GC停止工作,应用程序继续执行。
G1 Mix GC
Mix GC不仅进行正常的新生代垃圾回收,同时也回收部分后台扫描线程标记的老年代分区。
(三)垃圾回收器组合方式
垃圾回收器按照回收区域来划分可以分为:年轻代垃圾回收器、老年代垃圾回收器
按照收集方式来划分可以分为:串行回收、并行回收、并发回收、垃圾优先
组合方式:
对于垃圾回收器的选择:
单CPU或小内存,可以使用SerialGC,-XX:+UserSerialGC
多CPU,需要大吞吐量,可以使用ParalellGC或者ParalellOldGC,-XX:+UseParalellGC或-XX:UseParalellOldGC
多CPU,追求低停顿,可以使用ParNew或CMS,-XX:UseParNewGC或-XX:UseConcMarkSweepGC