jvm垃圾回收

1.什么是垃圾对象?

1.引用计数法:

使用引用计数法的会产生循环引用的情况, 但是理论上应该被回收的, 但是有没有对象可以对他进行调用., 引用计数法默认引用还是大于0 的,不会被回收.

2.根可达算法

gcroots 定义的 局部变量, 静态变量, 常量, jni指针等等 这些都是

然后从这写变量中间找可达性关系. 如图中所示的: 其中箭号可达的就是不能回收的, 他就是一种链路关系图

java中JVM使用的就是根可达算法

image-20211010091909749

2.垃圾回收算法

1.标记清除

会产生内存碎片, 造成不连续的现象, 大对象无法进行分配(java中分配大对象需要占据连续的内存空间)

2.标记压缩

在标记和清除的过程中, 同时进行内存的压缩整理(就是将有用的整理到一块儿, 空闲的整理到一块, 行程连续的内存空间), 效率比较低

3.复制算法

没有碎片, 浪费空间

复制就需要开辟两个一模一样的大小的空间, 将标记的东西整理到另一个区域, 剩下的就滚吧, 相对来说比较占用内存

image-20211010101823999

3.垃圾回收模型

java当中的垃圾回收器如下, Serial, Serial Old, ParallelScavenge,ParallelOld, ParNew和CMS, G1等等, 其中前面六个是数据分代模型,后面的都属于分区模型垃圾回收器.

image-20211010214635624
1.分代模型

分代模型: 新生代:复制算法,老年代:标记整理,标记清除算法,

在这里插入图片描述

我们主要回收的空间主要是堆, 分代模型的几乎所有的对象都是在堆中进行分配, 对是java中非常占据内存的空间, 当我们使用某个对象的时候, 我们会先new出来, 或者spring自动帮我们new出来的, 然后就不管了, 而当我们不使用的时候, 他就没有引用占用这个对象了, 这个时候就需要垃圾回收器来进行自动回收,.

为了提高垃圾回收的效率, 它会把整个堆空间一分为二, 前一段是新生代, 后一段是老年代, new出来的都会在新生代, 但是很快啊, 就会满了 , 满了之后会进行垃圾回收, 同时他的年龄也会加1, 回收不动的时候就会抛给from区 或者to区, 再次进行来及回收, 第二次, 第三次, 直到年龄够了, 最终还没有回收的会进入老年代, 那老年代也满了的时候, 就会进行一次Full GC,

新生代进入老年代的年龄一般是: 15,CMS是6,具体的需要看对象头.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Dwbb2xU-1634976311728)(https://cdn.jsdelivr.net/gh/hx1098/Algorithm@master/img/jvm/20211017135906.png)]

2.分区模型

在这里插入图片描述

4.不同类型收破烂的

1.serial + serial old

这些是单线程垃圾回收器, 所以内存满了的话, 业务线程就会产生停顿(STW), 让GC线程开始工作, 清扫完之后然后再进行业务的操作.

举个栗子: 这就好比一家公司里面有好多人在进行工作, 保洁的阿姨来清理时候, 所有的人都要停下所有的手头的工作, 跑到外面来配合保洁阿姨的工作, 等到保洁阿姨工作完了, 我们才可以进行下面的业务工作, 是不是有点烦(阿姨也没办法的, 工作需要)

所以现在的 serial 的可以说是废弃的状态.

image-20211012065320597
2.PS+PO垃圾回收器(吞吐量优先)

​ 举个栗子, 单线程的垃圾回收器好比你是一个人住, 买了个小房子(前提是你得有钱, 不过租的也行, 没毛病), 自己一个人打扫, 然后慢慢的有有了女朋友, 有了孩子, 把父母也接过来住了, 这个时候不够住了呀, 房子太小了多尴尬呀, 就直接换了一个大的房子住, 但是你自己清扫的时候就清扫不过来了呀, 你就呼朋唤友, 叫上你的家人一起清理, 这就是并发清理了.

​ 但是可以看到, 业务线程和GC线程任然是不在一起执行的, 只是会减少STW的时间,原来10秒, 现在可能变秒nan了, 反正就是快嘛, 但是也会有缺点, 你需要跟你的服务器cpu相匹配才行, 不然线程开的太多, 增加了cpu的上下文切换还是会变慢的.

​ 所以并发并不能解决STW的时间, 只能不断的优化stw的时间.

image-20211012065507302
3.CMS(Concurrent Mark Sweep)
1.cms的描述

随着业务的不断增加, cpu和内存的技术革新, 原来内存也不是太大, 也没啥问题, 但是一旦内存在8G, 10G以上的时候, 他的停顿时间是没有保障的, 就催生出了一个新的垃圾回收器.

并发标记清理垃圾, 一般来说CMS(回收老年代)和ParNew(新生代并发)组合使用, 但是通过下面的例子可以看到, 这家伙也是没有解决STW的停顿现象(貌似还没有垃圾回收器能做到吧, 多多少少都有停顿时间), 在初始标记阶段和重新标记阶段, 他还是会自己玩的, 只是一定程度上减少了STW的这种现象.

有小伙伴问了, 问啥需要重新标记呢? 因为前面已经有业务线程和GC线程在跑了, 后面清理的难点在于: 再初始标记之后你不知道又产生了多少个垃圾, 产生了多少个引用的变化, 这些引用还有没有用, 需不需要进行回收. 这就好比你家狗子就是那个业务线程, 早上你把家里收拾的好好的, 整整齐齐, 干干净净的, 但是你下班之后你不知道你还能不能认得出来是不是你家一样, 狗子在家可能还是会产生垃圾, 有可能是沙发掏了个洞, 也有可能是卫生纸碎片铺满了一地, 这都是垃圾的不确定性.

image-20211011225538755

所以为了实现并发的垃圾回收, 这里采用的是三色标记(Tri-color marking),

2.三色标记:
概述:

https://blog.csdn.net/u013490280/article/details/107495053

https://en.wikipedia.org/wiki/Tracing_garbage_collection#TRI-COLOR

感谢这位老哥的维基百科出来的好东西.其实还是不够通俗易懂的, 图中A对象的引用指向了B对象, 同时B对象里的有一个引用指向了C对象, 现在找到一个与A对象有关联的 ROOT:

  • A对象是黑色: 黑色说明A自身已经被扫描了, 以及他的下一个孩子也被扫描了
  • B对象是灰色: 灰色说明B已经被扫了, 但是孩子对象还没被扫描
  • C对象是白色: 没有被扫描的对象

在这里插入图片描述

问题1:浮动垃圾

情况: 如下图: 如果A到B到C的过程中都已经标记完成黑色了, 但是业务线程此时已经执行完成了, C已经没有引用的时候, 此时C还是黑色的状态,但是在三色标记当中当中, 我认为黑色的对象是可达的, 是存活对象不是乐色(垃圾), 此时C的状态就是浮动垃圾

解决方案: 在CMS当中是等待下一次被标记的时候才会进行垃圾回收, 同时也会设置出发GC的条件, 比如90%, 预留10%的空间用来存储的就是浮动垃圾, 举个栗子: 你在外面作为一个为了老板能开上宝马的打工人, 有一天朋友出差, 他的房子到期了, 把他的衣服家居啥的一股脑的全部搬你家里来了, 这些东西他也有用, 但对于你来说也没有用. 到了下一次垃圾回收的时候才知道有用没用.

问题2: 漏标问题

漏标的解决方案有两种, 一种是增量更新, 另一种是SATB堆栈快照的方式, 具体的请往下看G1垃圾回收器的详细描述.

情况:

时刻1 : 是一个正常的标记过程,A–>B–>C, A是黑色, B是灰色, C是白色, 仅仅标记到了B节点

时刻2 : 也挺正常的, 只不过标记到了B的时候发现B没有孩子了, 那就算标记完成了, 直接就将B标记为黑色了, 而A又将引用指向了原来的C, C原来就是白色, 而此时的A已经是黑色标记完成了, 那我默认A这一串都是被标记完成的, 但真实的情况是C还没有被标记, 这就是三色标记的漏标问题.

image-20211017155015861

解决方案:

​ 这里是CMS清理垃圾的过程, 初始标记只标记了跟"ROOT"直接相关的对象(A), 而并发标记阶段标记的是BCDE的节点, BCDE中使用了三色标记, 中间可能产生漏标的问题, 所以我就需要再次产生一次"重新标记"的过程, 这次重新标记处理的就是漏标问题.

image-20211017161728280

Incremental Update

但是但是但是CMS仍然不能彻底解决漏标问题, 就用到了下面的技术: Incremental Update(增量更新), 在时刻一到时刻二的过程中, 如果检测到黑色对象A增加了一个引用, 那就将A置为灰色, 而灰色代表还没有扫描完成, 就会去扫描C这个白色的对象, 扫完之后A就又变回了黑色的状态, 那么缺点也比较明显:

  • 无法处理浮动垃圾, 三色标记的增量更新实际到最后变成了多标, 变成浮动垃圾, 只能下次回收
  • 回收时间长, 业务线程和回收线程是同时进行的
  • 会产生大量的空间碎片,标记清除的特点

(这里还有个有趣的问题: 增量更新到底有没有彻底解决漏标的问题, 我在bilibili大学里看的一个视频, 说的是如果在多线程的情况下,有两个垃圾回收线程, 1和2, 线程1: A新增了C, 此时A会变成灰色,C为白色, 这个时候线程2并不感知,线程2继续扫描完成变成黑色, 认为我只有一个孩子, 将A变成黑色, 此时两个线程都扫描完成, C仍然为白色, 还是漏标了, 所以只能重新扫描, 会造成效率不稳定, 但是我查了很多资料, 都是找不到有关增量更新的漏标问题, 如果有朋友看到的话, 麻烦回复我一下, 让我开一下眼)

image-20211017162314470
4.G1垃圾回收器
image-20211019213729580

​ G1垃圾回收器之前, 垃圾收集的目标范围要么是整个新生代(MinorGC), 要么是真个老年代(MajorGC), 再要摸就是整个java堆(FullGC),而G1很是另类, 他开创了基于region的堆内存布局, 图中的region, 每一个不是固定的, 都会根据需要扮演新生代的Eden空间, Survivor空间,或者老年代空间, 收集器能够对不同角色的region使用不同的策略进行去处理, 无论是新生对象, 还是存活了一段时间的对象, 都能获得很好的收集效果.

其中region中还有一个Humongous区域, 专门用来存储大对象, 一般来说如果超过了Region容量一半, 就算是大对象了, region的大小可通过参数进行设置(1~32M), 这里不过多赘述, 对于那些超过整个region的大对象, 会被存在连续的Humongous Region中, G1大多数行为会将其看为老年代.

G1保留了老年代新生代的概念, 在回收时候, 会优先回收价值收益最大的那些region(价值即回收能获得的大小和回收所需时间的经验值大小), 谁回收的时间短, 回收的还多, 我就去那家(region)去收垃圾, 这样我回收的空间就多, stw的停顿就可以进行预测.

image-20211023154609434

​ 接下来看看G1垃圾回收器是如何处理漏标问题的, G1垃圾回收器使用的是 STAB(snapshot at the beginning), 接下来开始进行操作了, 坐好了, 时刻1到时刻2业务代码进行了一下操作, A新增了引用C, 删除了B到C的引用, 这个时候, 把这个引用推到了GC的堆栈, 到了时刻2之后, GC线程在堆栈找到C, 知道C被漏掉了, 我再看看有没有对象指向你呢, 原来还有一个A的引用指向了C, 所以这个C就不会被漏掉了. 可以看到, 这个过程最主要关注的是"引用的消失", 而不是引用的增加.

感谢:

https://my.oschina.net/u/3471412/blog/4740055

https://www.cnblogs.com/hongdada/p/14578950.html

https://blog.csdn.net/qq_39685066/article/details/107294318

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值