标记阶段算法:
找出哪些是需要清除的对象
引用计数法(基本废弃)
对于一个对象A,如果一个对象引用A了,那么A的引用计数器+1;当引用失效了,那么A的引用计数器-1,只要对象在引用计数器计数为0的时候,表示A可以回收
优点:实现简单,垃圾对象容易识别,判定效率高
缺点:
1.需要单独的空间记录引用计数,耗费空间
2.每次引用和引用失效都要计算引用计数,耗费少许性能
3.致命的是无法处理循环引用的问题
可达性分析算法/根结点搜索算法(目前常用)
WechatIMG10.png
所有能GC Roots 引用的对象或者间接引用的对象都是不可回收的对象,相反,没有被直接或者间接引用的对象,为可回收的对象,
可达性算法要求一致性,所以在标记的过程中,会暂停JVM里面的用户线程STW,注意这里是标记所有的可达对象(不可回收对象),
在对象的head里面记录,回收的是那些没有标记的对象
GCRoots包括哪些:
1.栈帧中局部变量表中的引用对象
2.本地方法栈中引用的对象
3.类属性静态属性引用的对象
4.所有被synchronized持有的对象
5.JVM里面的内部引用,包括异常类对象,系统类加载器对象等
清除阶段算法
标记清除法
WechatIMG11.png
分为两个阶段
1.第一个是标记阶段,即可达性标记算法,对根对象对象进行遍历,这里是标记非垃圾对象,记录在对象的head中
2.第二个阶段清除,对堆中所有的对象进行遍历,发现head中没有被标记,则清除
优点:比较基础,比较好理解
缺点:
效率不算高,标记需要遍历,清除要遍历
需要STW,导致用户体验差
清除的时候会会产生空间碎片
需要注意的是,这里的所谓的清除并非置空,而是将可回收的地址记录在空闲列表中,下次有新对象加载的时候直接覆盖
复制算法
WechatIMG12.png
复制算法遍历根结点的直接引用或者间接引用对象,直接复制到另一块相同大小的区域,然后清空另外一片区域
优点:
没有标记清除的过程,效率高
复制后的空间地址连续,没有空间碎片
缺点:
需要两倍的空间
需要注意的是该算法适用于大部分对象需要回收的区域-新生代
标记压缩算法/标记整理算法
WechatIMG13.png
是标记清除算法的优化步骤如下,前两步和标记清除相同
1.第一个是标记阶段,即可达性标记算法,对根对象对象进行遍历,这里是标记非垃圾对象,记录在对象的head中
2.第二个阶段清除,对堆中所有的对象进行遍历,发现head中没有被标记,则清除
3.堆存活对象进行压缩移动,形成一片连续的空间
优点:
解决了标记清除空间碎片问题
解决了复制算法的空间浪费问题
缺点:
执行效率来说.要低于以上两种算法
移动的过程中需要STW,因为对象的地址发生了移动
JVM里面实际应用算法
基础的算法都介绍完了,具体问题具体分析,需要多种基础算法结合的使用,基本不单一使用
分代算法
1、新生代
新生代包含一个eden区,两个survivor区,默认比例为8:1:1。
新创建的对象基本都会存放在EDEN区(大对象直接放在老年代),而这部分对象大部分都会“朝生暮死”,使用后被快速回收。常规一次回收可回收70%-95%的空间,效率非常高。
新生代采用复制算法进行回收,假如当前正在使用的是eden及survivor1区,大部分新生对象都会放在eden区(大对象会直接放在老年代),当eden区内存满了以后,会将存活对象保存到survivor2区,eden进行Minor GC。survivor1区的存活对象根据年龄(默认是15,每经历一轮GC,年龄加1)决定去向,年龄达到阀值,复制到老年代,若年龄未到,复制到survivor2区,survivor1区清空。如果survivor2区内存不足以存放所有的存活对象,则需要以来老年代的担保机制将部分对象复制到老年代。
2、老年代
新生代与老年代默认比例为1:2,老年代用来存放存活时间较长,但还是会死的对象信息(如缓存对象、单例对象等)。老年代对象来源有一下几个方向:
①大部分来自于年青代,对象在年青代存活时间过阀值,就会被复制到老年代。
②年青代中部分对象虽然为过阀值,但是因为survivor区已满,由担保机制复制到老年代。
③部分大对象直接在老年代创建,不经历年青代,如长字符串、长数组等需要大量连续空间的对象。
老年代一般采用标记-整理算法或标记-清除算法,如果采用新生代的复制算法,效率较低。老年代对象存活时间较久,回收频率较低。
增量收集算法(使用少)
基本思想:一次性收集的垃圾过多,可能会造成长久的停顿(STW),可以让应用程序线程和垃圾回收线程交替使用,即一次收集一部分垃圾,
接着切到实际应用程序,直到垃圾收集完成为止,使用的还是复制算法和标记清除算法的结合
缺点:来回切换,必然加大系统性能损耗,垃圾回收成本增加,降低系统吞吐量
分区算法(G1垃圾收集器主要算法)
WechatIMG14.png
基本思想:
一般相同情况下,堆空间越大,垃圾回收的时间就越长,GC的停顿时间也越长,为了更好的控制GC的停顿时间,将一个大的区域划分
成若干个小区间,每次合理的收集若干个小区间,从而减少一次GC带来的停顿
垃圾回收器
Serial月Serial Old 垃圾回收器
WechatIMG17.png
串行垃圾回收器,新生代用Serial,采用的是复制算法,老年代用Serial Old 采用的标记压缩算法,适用单核计算机,是Hotstop虚拟机client端默认的垃圾回收器
在server的Hotstop中,是CMS 的备用方案
优点:单核下简单高效,适用于client
开启命令:-XX:+UseSerialGC
ParNew GC
WechatIMG18.png
并行垃圾回收器,和串行回收器的主要区别是并行执行,一般并行用在新生代,老年代一般用Serial Old串行
优点:在多Cpu的情况下效率比Serial高
开启命令:-XX:UseParNewGC
-XX:ParallelGCThreads 设置并行回收线程数,默认和cpu数量相同
Parallel Scavenge与Parallel Old
WechatIMG19.png
Parallel Scavenge与Parallel Old是JDK1.8默认垃圾收集器组合
Parallel Scavenge 和ParNew 的主要区别在于Parallel Scavenge可以动态的分配资源,适用于高吞吐量的系统,适用于后台系统,可以在吞吐量和低延迟间获取平衡
WechatIMG21.png
WechatIMG22.png
CMS
WechatIMG23.png
CMS是老年代的垃圾回收器,主要分为四个阶段
1.初始标记:所有用户线程暂停(STW),该阶段仅仅GCRoots能直接关联的对象,直接关联的对象比较少,一旦完成,用户线程恢复,STW时间很短
2.并发标记:从直接关联对象找到间接关联对象,这个过程比较长,但是是和用户线程并发执行的
3.重新标记:由于并发标记是和用户线程并发执行的,避免不了有些对象的标记发生变化,需要进行修正,该阶段会STW,会比初始标记时间长点,但是比并发标记时间少的多
4.并发清理:此阶段是清理要回收的对象,因为不移动对象地址,所以可以和用户线程并发执行
优点:低延迟,并发收集
缺点:
1.使用标记清除,会有空间碎片
2.并发执行,会长时间占用系统资源,降低吞吐量
3.无法收集浮动垃圾即初始标记后产生的垃圾
注意:使用不多,缺点很多,一般和ParNew(新生代)使用,jdk9之后标记为废弃,jdk14删除了,这里就不贴参数
G1(Garbage First)
开启命令:-XX:UseG1GC Java9之后默认
为什么叫Garbage First?
1.因为G1是一个并行收集器,把堆内存分成不同很多不相关的区域(Region)(物理上不连续),用不同的Region来表示Eden,S1,S2,Old区域
2.避免了Full Gc,会跟踪分析每个Region的价值大小(分析每个Region的回收空间和回收时间),在后台维护一个优先列表,根据设置的回收时,优先回收价值大的Region
3.该垃圾回收器回收的是最大价值的Region,所以成为Garbage First(垃圾优先)
传统的堆存储模型
G1垃圾回收器的堆存储模型
G1垃圾回收器的特征
并行与并发
1.并行:G1垃圾回收器在回收期间是多个垃圾回收线程一起工作的,会发生STW
2.并发:回收的部分阶段是和用户线程并发执行的
分代收集
1.G1依然属于分代型垃圾回收器,会区分Eden,S0,S1,Old区,但是物理上和逻辑上不要求连续,不再固定大小和数量
2.Eden,S0,S1,Old会以多个Region的形式存在
3.和之前的介绍的垃圾回收器不同,G1是兼容老年代和新生代的垃圾回收器(之前的要不是是新生代,要不是老年代)
空间整理
Region和Region之间采用的复制算法,整体的可以看错是标记整理算法,两者结合,可以防止空间碎片,适用于堆比较大的应用
可预测的停顿时间模型
可以设置垃圾回收的期望时间,由于Region模式我们可以选择性的回收Region,每个Region会有一个价值参数,存在一个列表,从高到底存放着每个Region的回收价值,会首先回收高价值的Region
缺点:空间占用大,适用于大堆,小堆不如CMS(平衡点在6-8G)
参数设置
G1垃圾回收器的回收过程
1.新生代GC(young GC)
每个Region代表一个角色,这个角色不是一尘不变的,产生GC的时候会清空可能会赋予其他角色当代表Eden区的所有Region满的时候,会触发YongGC,采用的是复制算法,将代表Eden的Region和所有代表S0的所有Region复制到新的S1区,该阶段也是STW
2.老年代并发标记过程(Concurrent Marking)
当堆的使用率在45%以上的时候会触发并发标记过程
3.混合回收(Mixed GC)
如果必要时候FullGc还是存在的
Rset
每个Region初始化时,会初始化一个remembered set(已记忆集合),这个翻译有点拗口,以下简称RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。
image
Region1和Region3中有对象引用了Region2的对象,则在Region2的Rset中记录了这些引用。
作用:
通常我们寻找垃圾回收对象都是可达性分析算法未标记的对象.对于每次标记来说都需要遍历所有的GCRoots,这样其实是标记处整个堆的不可回收对象,对于堆很大的时候,会极其耗费效率.G1采用的就是Rset机制来避免全堆扫描问题,每个Region对应一个Rset,每次在写入对象的时候,检查该对象是否有不在该Region的引用,如果有就记录到引用对象的Region的cardTable中.高效的利用Rset回收
详细链接:https://www.jianshu.com/p/870abddaba41
附加概念
finalize方法
在对象即将回收的情况i啊调用的方法,可以通过重写Object里面的finalize()方法
1.可以复活对象
2.回收系统资源,
该方法只能执行一次,哪怕已经复活了,也只会调用一次
引用类型
强引用:普遍是有的new 对象都是强引用,如果强引用关系存在,垃圾回收永远不回收
软引用:系统即将发生OOM的时候,会将这类对象回收
弱引用:弱引用对象只能活到下一个垃圾回收前,只要发生垃圾回收,就会回收这类对象
虚引用:一个对象有无虚引用完全对该对象生命周期不构成影响,关联虚引用的唯一作用就是该对象能在垃圾收集器在收集之前得到一个系统通知
内存泄漏
WechatIMG16.png
就是对象已经不再使用了但是还是被GCRoots间接或者直接关联着,即无法垃圾回收,内存泄漏的累加结果是OOM
举例:
1.前面说到的循环引用就是典型的内存泄漏的例子,但是JVM里面没有使用引用计数法,所有这种情况在实际程序中不会出现,
2.单例对象:基本上我们的单例对象创建了,生命周期很长,单例对象关联的对象如果不使用了会一直存在,这就造成了内存泄漏
3.调用外部资源,流,数据库链接池等如果不close掉会造成内存泄漏