JVM垃圾回收算法

Java技术债务

共 2783字,需浏览 6分钟

 · 2022-02-10

概述

JVM中,程序计数器虚拟机栈本地方法栈都是都是线程私有的,随线程而生随线程而灭,栈帧(栈中的对象)随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。

因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

判断对象是否存活

引用计数:记录每个对象被其他对象所持有的引用数,被引用加一,引用时效减一。计数器为0则表示不可用。很难解决对象之间相互循环引用问题。(目前已经不用:不能解决循环依赖的现象)可达性分析算法:从GC root对象向下搜索其所有走过的路径为引用链,当一个对象不再被任何的GC root对象引用链相连是说明对象不再可用。

在 Java 中可以作为 GC Roots 的对象有以下几种

虚拟机栈中引用的对象方法区类静态属性引用的对象方法区常量引用的对象本地方法栈 JNI 引用的对象


OopMap数据结构存储GCRoot对象,但是随着系统的运行会导致OopMap会逐渐变大,所以也并不会存储所有的GCRoot对象,而是在一个所谓的安全点进行记录GCRoot对象。

垃圾回收算法

标记-清除算法

“标记-清除”(Mark-Sweep)算法,算法分为“标记”和“清除”两个阶段:   

    首先标记出所有需要回收的对象    在标记完成后统一回收掉所有被标记的对象。

之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:   

    一个是效率问题,标记和清除过程的效率都不高;    另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

ce4a4b49a0c6e0a11f52d5b45c167a4d.webp

复制算法

“复制”(Copying)算法,它将可用内存按容量划分为大小相等的两块每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点

    这种算法的代价是将内存缩小为原来的一半,降低了内存的利用率,持续复制长生存期的对象则导致效率降低。    在对象存活率较高时就要执行较多的复制操作,效率将会变低

ff1e0f4ec50ce67f4cfb14ec6083e4f0.webp

标记整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,老年代一般不能直接选用这种算法(老年代一般是存活时间较长的大对象)。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这种算法克服了复制算法的低效问题,同时克服了标记清除算法的内存碎片化的问题;

d0e6da9c4c9253e319ef6af4f504c2c7.webp

分代收集算法

“分代收集”(Generational Collection)算法是一种划分的策略,把Java堆分为新生代老年代

根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

对象存活率低,使用复制算法,分为:Eden区,Survivor区(Survivor from/to)比例为8:1:1

新产生的对象首先进入Eden区,当Eden区满了使用Survivor from当Survivor from满了进行新生代GC(Minior GC),首先将Eden区和Survivor from区存活的对象复制到Survivor to区,然后清空Eden和Survivor from区,此时Survivor from区变成Survivor to区,Survivor to 变成Survivor from区;如果复制的时候发现Survivor to无法容纳全部存活对象,则根据老年代的分配担保将对象copy进老年代,如果老年代无法容纳,则进行Full GC老年代GC。

1、大对象直接进入老年代:可根据JVM参数配置-XX:PretenureSizeThreshold,大于配置值即可,这样防止Eden区和Survivor区频繁的内存复制2、长期存活的对象进入老年代:可以根据JVM参数配置年龄,每进行一次GC并且能被Survivor容纳,则年龄+1,如果年龄达到配置值,对象会进入老年代,参数配置:XX:MaxTenuringThreshold;JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代——如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。

为什么要有两个Survivor:

主要是为了解决内存碎片化和效率问题。如果只有一个Survivor时,每触发一次minor gc都会有数据从Eden放到Survivor,一直这样循环下去。注意的是,Survivor区也会进行垃圾回收,这样就会出现内存碎片化问题。

碎片化会导致堆中可能没有足够大的连续空间存放一个大对象,影响程序性能。如果有两块Survivor就能将剩余对象集中到其中一块Survivor上,避免碎片问题。





f84351f0d16ade1a84d3f2670de77984.webp

JVM内存泄漏和内存溢出的原因
JVM常用监控工具解释以及使用

Redis 常见面试题(一)

ClickHouse之MaterializeMySQL引擎(十)

三种实现分布式锁的实现与区别

线程池的理解以及使用


号外!号外!最近面试BAT,整理一份面试资料,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。想获取吗?如果你想提升自己,并且想和优秀的人一起进步,感兴趣的朋友,可以在扫码关注下方公众号。资料在公众号里静静的躺着呢。。。



喜欢就分享
认同就点赞

支持就在看

一键四连,你的offer也四连a6fefd839284cb0245a2cdcfe58f9ef8.webp


本文作者:Java技术债务 

原文链接:https://www.cuizb.top/myblog/article/1644407284

版权声明:本博客所有文章除特别声明外,均采用 CC BY 3.0 CN协议进行许可。转载请署名作者且注明文章出处。


浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报