一、垃圾回收器
1.1 哪些对象可以回收?
1.1.1 引用计数算法
引用分析算法指的是对每一个对象进行计数,对象被引用时进行+1,引用失效时进行-1,当进行垃圾回收时,如果计数为0
1.1.2 可达性分析算法
可达性分析指的是对象如果可以通过被引用找到属于根节点的对象,那么任务该对象不可回收
-
属于根节点的对象: GCroots
-
虚拟机栈中局部变量表所引用的对象
-
静态属性引用的对象
-
常量引用的对象
-
本地方法引用的对象
-
同步锁持有的对象
1.1.3 对象回收过程
如果该对象重写了finalize()方法,回收过程会执行finalize() 方法,但是jvm只会执行一次finalize()方法,可以在这个方法中对该对象进行引用,那么此时该对象就不会被回收。如果下一次在被回收就不会执行finalize()方法
1.2 垃圾收集算法
1.2.1 分代理论
三个假说:
1.2.2 标记-清除算法
标记-清除算法的两个过程:
-
判断该对象是否需要回收,不需要回收的在对象头header进行标记
-
该阶段并不是真正意义上的垃圾回收,而是将没有标记的对象放入free_list中,新建对象是就从该list中拿出一块合适的空间。对于空闲列表中的内存查询一般有以下三种策略:
-
First_fit : 遍历free_list,返回第一块大于等于需求内存大小的内存地址
-
Best_fit :遍历free_list,找出所有符合需求内存大小的内存地址并返回最小的一块内存
-
Worst_fit : 遍历整个free_list ,找出最大的内存分块对于需求内存大小进行切割,并返回切割的内存地址。
优点:
-
逻辑简单
-
不需要移动对象,不需要进行移动对象后的操作
但是该算法存在一定的缺陷:
1.2.3 复制算法
标记-复制算法:对于内存空间进行划分,每次只使用一般的内存,当需要进行垃圾回收时,将不需要的回收的对象放入另一部分内存中。这样就解决了内存碎片化问题
缺点:
Apple式标记复制算法,使用eden,ser区的方式,当不需要回收的数量大与10%,就会出现异常
1.2.4 标记-整理算法
标记-整理算法过程:先对对象进行标记,然后对不需要的对象进行移动到内存的另一端,然后删除不需要的对象,当进行垃圾回收时,会STW。
-
在垃圾回收过程中需要移动对象,效率降低
-
整理过程中的算法:
可查看的网页链接:https://www.jianshu.com/p/698eb5e1ccb9
1.2.5 垃圾回收算法细节
-
GC根节点枚举
在进行标记的过程中通过可达性分析判断对象是否存活,因此需要对GC根节点进行枚举,然后再通过引用链获取存货对象进行标记。为了保证这个过程中对象引用不发生变化,需要进行STW,因此通过安全点和安全区域的方式去解决引用一致性,使用GCmap快速查找。
在hotstop中,使用OOPmap的方式减少根节点枚举时间。在类加载的时候,就会计算出当前对象中进行了那些引用,在编译过程中,也会记录在特定的位置记录栈和寄存器中存在引用。因此在进行根节点枚举时,就可以判断出哪里有引用,从而提高枚举效率
-
安全点
-
安全区域
-
记忆集与卡表
当存在跨代引用时,为了保证跨代引用中的对象不被清除,但是有不去检索整个老生代,因此在新生代中开辟了一段空间,用于 存储非收集集合指向收集集合
-
写屏障
类似于环绕通知的方式,在对象复制前后对卡表进行操作
如果多线程对卡表对于的同一地区进行操作,那么就会存在冲突,并且操作的次数也过于频繁,存在通过或者无效化的问题,因此需要进行有条件的写屏障,也就是如果当前卡表元素没有变脏,才会变脏。但是由于会进行是否变脏判断,也会消耗性能。在1.7之后,提供了-XX:+UseCondCardMark参数进行有条件的写判断 (伪共享) (QUESTION:伪共享)
-
标记过程中存在的问题
在查询引用链过程中,如果在这个过程不是STW后进行的,那么并发的情况下,进行引用增加和引用解除,就会出现两个问题:
1.3 经典的垃圾回收器
1.两个收集器之间有连线,表明它们可以搭配使用。
2.其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。
3.(红色虚线)由于维护和兼容性测试的成本,再JDK8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃,并在JDK9完全取消了这些组合的支持,即移除。
4.(绿色虚线)JDK14中,弃用Parallel Scavenge+SerialOld组合。删除CMS垃圾回收器。
1.3.1 Serial收集器
在单线程的垃圾回收器中,serial算是效率最高的垃圾回收器了。基本上200M的内存回收也只需要100ms以内
新生代与老年代使用serical与Serial Old收集器:-XX:+UseSerialGC
1.3.2 ParNew收集器
使用ParNew收集器参数:
1.3.3 Parallel Scavenge收集器
1.3.4 Serial Old收集器
Serial给老年提供的一块收集器,其特点与serial一样,但是采用的是标记整理算法。
Serial Old是运行在Client模式下默认的老年代垃圾回收器。Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用 ②作为老年代CMS收集器的后备来及手机方案。
1.3.5 Parallel Old收集器
1.3.6 CMS收集器
针对老年代的收集器,大致过程是:
-
初始标记:标记根节点直接相连的对象
-
并发标记:查询整个引用链
-
重新标记:使用增量更新的方式,进行再次标记
-
并发清理:清除标记的垃圾
值得注意的是初始标记和重新标记需要STW,CMS收集器也存在一定的弊端:
-
由于在并发标记和并发清除过程中需要使用多线程的方式,因此需要花费部分线程,用户的可使用的线程减少,就使得吞吐量降低,默认并发使用的线程数量是
(CUP核心数量+1)/4
-
由于在收集过程中使用的是增量更新的方式处理并发的可达性分析,会存在在并发过程中失去引用链的对象,这些对象被称之为浮动垃圾,如果一次收集过程中浮动垃圾太多,导致并发过程中有新的对象没有空间可以使用,那么就会再次触发Full GC,因此将Serial old收集器作为CMS的备用收集器。但也会造成STW,是的体验感下降。
-
由于CMS使用的是标记清除算法,因此会出现内存碎片,当内存不够时会触发full GC,因此CMS提供了两个参数:
可查看链接:https://blog.csdn.net/weixin_44568697/article/details/109030617?spm=1001.2014.3001.5501
1.3.7 G1收集器
一款基于局部收集的收集器,可以指定收集所需要的时间,增加吞吐量的收集器。可以建立停顿时间模型。而实现停顿模型的方式就是用过对内存进行分块形成Region而实现的。也就是说对于G1回收器没有固定的内存认定是那个分代,对于新生代,老生代等都是动态划分的。一般来说一个Region的内存大小是1-32MB之间的2的倍数。如果说对象的大小超过了Region的一半,会使用到内存较大的Humongous Region进行存储,如果大于region的内存,就会使用多个连续的Humongous Region存储。、
因此G1收集器也有以下的问题需要妥善考虑:
-
跨代引用问题,还是使用记忆集,但是相比于CMS只有一个记忆集,G1对于每一个Region都会维护一个记忆集,并且key时内存开始的地址指针,value是一个卡表,可以理解为这个记忆集不仅仅有我指向谁,还会记录谁指向我。
-
并发标记过程中的用户创建对象引用与引用解除,使用原始快照SATB的方式进行处理的,对于用户创建对象的地址分配是使用两个指针TAMS,可用的地址在这两个指针指向的内存之上即可分配。
-
如何建立时间停顿模型。
G1收集器大概的过程如下:
-
初始标记:标记根节点直接相连的对象,由于对象的分配会重新指定TAMS的两个指针
-
并发标记:查询整个引用链,并使用SATB原始快照的方式处理并发上的用户线程对象的引用与解除
-
重新标记:使用原始快照的方式,进行再次标记
-
筛选回收:垃圾回收器通过对每个区域进行分析,筛选出需要进行回收的区域,然后将该区域不需要删除的对象复制到一个空的Region区域中,并将原来的区域中的数据全部删除清空,由于需要移动对象,需要进行STW
需要注意的是的复制移动的对象之后需要进行引用的移动
在Eden区与Survivor1区数据收集方式:
-
查询所有的根节点已经Rset中的数据
-
更新Rset中的数据以保证数据的引用链的准确性
-
处理Rset,Rset中Eden区被引用的数据都视为隐藏标记,不进行垃圾回收
-
进行复制,将Eden区中和Survivor1中的不需要回收对象防区Survivor2中
-
引用处理,将指向原Eden区的引用指向新的位置
在老年代的数据回收过程:
-
GCRoot的直接引用对象遍历,STW
-
获取所有Survivor2d在老生代中被引用的关系,为什么需要在Young GC之前完成?
-
引用链的完全遍历
-
再次标记,使用原始快照的方式进行再次标记
-
独占清理:通过计算分析每一个region中的数据,获取最合适的停顿时间模型的区域进行分析 STW
混合收集过程:
在了解该收集器之前需要知道该收集器对内存的划分,把内存划分成2的次方大小区域,区域的大小是1M-32M,该收集器也使用了分代策略,但是分代方式更为灵活,每一个区域都可以作为新生代或者老生代。如果存在了超过区域大小一半的对象就称之为大对象,使用Humongous区域进行存储,如果对象的大小超过了区域的大小,就会使用N个连续的HUmongous区域来存储,G1中很多的操作会将Humongous区域视为老生代。
由于设置了收集时间,因此需要保证在该收集时间内完成垃圾收集,如何去保证时间的可行度了,在垃圾回收时对每一个区域都进行数据收集,记录回收时间,卡表的脏元素数量等可以测量的值,然后通过衰减平均值进行分析,各个区域数据越新越有价值,因此然后对所有的区域和设定的回收时间进行分析,计算出可以回收的区域。如果回收时间设置过小,会导致每次回收只会回收少部分的区域,使得分配器分配的频率大于回收 的频率,从而频繁的触发full GC。
由于对内存进行分区之后,区域与区域之间跨代引用也出现了问题,因此对每一个区域进行卡表设置,相比CMS只对老生代进行卡表操作而言,G1收集器需要更多的内存空间和资源去维护每个区域的卡表。可能会多出10%~20%的内存占用。
由于收集过程是并行的,因此对于收集过程中存在的创建分配的对象,G1采用对每个区域分配一段小的空间进行存储这些区域,使用两个指针进行标记,TAMS(top at mark start),在垃圾收集过程中默认这些对象是被隐式标记过的。
G1收集器与CMS收集器的比较:
-
G1在收集过程中不会产生内存碎片
-
由于G1卡表机制,会耗费更多堆内存和其他资源
-
由于G1使用的是原始快照的方式来解决并发问题下的可达性分析,需要使用写前屏障来来跟踪引用的指针发生的变化,因此过程中还会产生别的消耗。
-
对于小内存的程序来说CMS可以很好的发挥作用,但是对于大内存来说,G1的性价比更高,这个平衡点大概是6~8G之间
-
CMS相比G1对于垃圾回收有更低的内存占用和运行负载(需要维护卡表和写屏障)
导致Full GC的原因有以下情况:
-
复制对象时没有空间可用
-
并发标记过程中由分配器分配的对象没有空间存储
1.4 低延迟收集器(暂时先不了解)
内存占用,吞吐量,延迟 不可能三角形
1.4.1 Shenandoah收集器
1.4.2 ZGC收集器
1.5 对象内存分配
1.5.1 对象年龄判断机制
正对parNew收集器与serail收集器,如果对象的大小大于一个指定值,就会直接放入老生代中。
如果Survivor区的中低于或等于某个年龄的对象占用内存综合超过了Survivor空间的一半,就会直接将对象放入老生代。
1.5.2 空间分配机制
如果老生代剩余空间小于新生代对象空间总和或者小于历代晋升到老生代对象的平均值,就会进行full GC
垃圾回收策略与经典垃圾回收器
原文:https://www.cnblogs.com/standbyme/p/15196716.html