当前位置: 首页 > news >正文

(五)Java虚拟机——垃圾回收机制

对比其他语言

C++/C(手动回收)

这类语言没有自动垃圾回收机制,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。

内存泄漏:指的是不再使用的对象再系统中未被回收,内存泄漏的积累可能会导致内存溢出

  • 优点:回收及时性高,由程序员把控回收时机。
  • 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题。

Java/C#/Go/Python(自动回收)

通过垃圾回收器对不再使用的对象完成自动的回收,垃圾回收器主要负责对上的内存进行回收。

  • 优点:降低程序员实现难度、降低对象回收bug的可能性。
  • 缺点:程序员无法控制内存回收的及时性。

应用场景:

  • 解决系统僵死的问题:大厂的系统出现许多系统僵死问题都与频繁的垃圾回收有关。
  • 性能优化:对垃圾回收器进行合理的设置可以有效地提升程序的执行性能。

方法区回收

回收内容主要就是不再使用的类

线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。

方法的栈帧在执行完方法之后,就会自动弹出出栈并释放掉对应的内存。

判定一个类可以被卸载,需要同时满足下面的三个条件:

  1. 此类所有实例对象都以及被回收,在堆中不存在任何该类的实例对象以及类对象。
  2. 加载该类的类加载器已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用。

可以手动触发垃圾回收,可以调用 System.gc()方法

注意:System.gc()仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收,Java虚拟机会自行判断。

堆回收

Java中的对象是否能被回收,是根据对象是否被引用来决定的。

判断对象是否被引用:引用计数法和可达性分析法。

引用计数法

应用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用减1。

引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法

缺点:

  1. 每次引用和取消引用都需要维护计数器,对系统性能有一定影响。
  2. 存在循环引用问,所谓循环引用就是当A引用B,B也引用A会出现对象无法回收的问题

以下段代码为例,A的对象里面引用B,B的对象引用A,将栈中的AB去掉,实例对象在栈中已经没有变量引用,由于A、B的引用计数器是1,不会被回收,出现了内存泄漏。

可达性分析法

可达性分析法将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。

如下图:A到B再到C和D,形成了引用链,可达性分析法指出,如果从某个到GC Root对象是可达的,对象就不可被会回收。

GC Root对象:

  • 线程Thread对象,引用线程栈帧中的方法参数、局部变量等。
  • 系统类加载器加载的java.lang.Class对象,引用类中的静态变量(eg:应用类加载器)
  • 监视器对象,用来保存同步锁synchronized关键字持有的对象。
  • 本地方法调用时使用的全局对象。

下面代码使用可达性分析法,实例对象AB在栈中已经没有变量引用,GC Root不能引用到AB的实例对象,AB会被回收。

对象引用 

  • 强引用:可达性分析描述的引用
  • 软引用:软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。(常用于缓存)
  • 弱引用:弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。
  • 虚引用:也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回 收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道 直接内存对象不再使用,从而回收内存,使用了虚引用来实现。(常规开发不会使用)
  • 终结器引用:指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后 由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该 对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。(常规开发不会使用)

软引用

软引用的执行过程:

  1. 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。
  2. 内存不足时,虚拟机尝试进行垃圾回收。
  3. 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
  4. 如果内存依然不足,抛出OutOfMemory异常。

垃圾回收算法(GC算法)

垃圾回收需要做的事情:

  1. 找到内存中存活的对象。
  2. 释放不再存货对象的内存,使得程序能再次利用这部分空间。

垃圾回收算法的评价标准

STW(Stop The World):Java垃圾回收过程会通过单独的GC线程来完成,不管哪一种GC算法,都会有部分阶段需要停止所有的用户线程。

判断算法是否优秀

  • 吞吐量:指的是CPU用与执行用户代码的时间与CPU总执行时间的比值,即吞吐量 = 执行用户代码时间 / (执行用户代码时间 + GC时间)吞吐量数值越高,垃圾回收效率越高。
  • STW:STW最大停顿时间越短,越好。
  • 堆使用效率:不用垃圾回收算法,对堆的使用方式不用。

这三中评价标准:吞吐量、STW最大停顿时间和堆的使用效率不可兼得。

一般来说,堆内存越大,STW最大停顿时间越长;减少最大停顿时间,就会降低吞吐量。

所以不用的垃圾回收算法适用于不同的场景。 

标记清除算法(整个内存处理)

标记清除算法分为两个阶段:

  1. 标记阶段,将所有存活对象进行标记。使用可达性分析法,从GC Root开始通过引用链遍历出所有存活对象。
  2. 清除阶段,从内存中删除没有被标记也就是非存活对象。

标记清除算法的优缺点:

  • 优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
  • 缺点:
    • 碎片化:由于内存是连续的,在对象被删除之后,内存中会出现很多细小的可用内存单元。如果需要一个比较大的空间,很可能这些内存单元的大小过小无法进行分配。
    • 分配速度慢:由于内存碎片化,需要维护一个空闲链表,极有可能发生每次需要遍历到链表最后才能获得合适的内存空间。

复制算法(一半内存处理)

复制算法的核心思想:

  1. 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用一半空间。
  2. 在垃圾回收GC阶段,将From中存活的对象复制到To空间。
  3. 将两块空间From和To名字互换。

复制算法的优缺点:

  • 优点:
    • 吞吐量高:复制算法只需要遍历一次存活对象复制带To空间即可
    • 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
  • 缺点:内存使用率低:每次只能让一半空间来为创建对象使用。

标记整理算法

标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。

核心思想:

  1. 标记阶段:将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
  2. 整理阶段:将存活对象移动到堆的一端,清理掉存活对象的内存空间。

标记整理算法的优缺点:

  • 优点:
    • 内存使用效率高:整个堆内存都可以使用,不会像复制算法只能使用半个堆内存。
    • 不会发生碎片化:在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间。
  • 缺点:整理阶段效率不高 ,比如Lisp2整理算法需要堆整个堆中的对象搜索三次。

分代垃圾回收算法

现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代回收算法。

分代垃圾回收将整个内存区域划分为年轻代和老年代。

年轻代:存放存活时间比较短的对象。

老年代:存放存活时间比较长的对象,是一整块区域。

分代回收时,创建出来的对象,首先会被放入Eden区

随着对象在Eden区越来越多,如果Eden区满了,新创建的对象已经无法放入,就会触发年轻代GC,称为Minor GC或Young GC。

Minor GC运用复制算法,使用的区域是S0和S1,把需要回收的对象回收。

注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。

如果Minor GC后对象的年龄达到阈值(最大值为15,默认值与垃圾回收器有关),对象就会被晋级升至老年代。

当老年代空间不足时,先尝试Minor GC,如果还是不足,就会触发FulL GC,Full GC会对整个堆进行垃圾回收。

特殊情况:

  1. 年轻代空间不足时,且经过Minor GC仍然不足时,会将年龄未达到的对象,放入老年代中。
  2. 老年代空间不足时,且经过Minor GC仍然不足时,将先前因年轻代空间不足提前放到老年代的对象,放回年轻代。

分代GC算法将堆分为年轻代和老年代注意原因:

  1. 可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。
  2. 年轻代和老年代使用不同的垃圾回收算法,年轻代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。
  3. 分代的设计中只允许回收年轻代,如果能满足对象分配的要求就不需要对整个堆进行回收,STW时间就会减少。

垃圾回收器组合——Serial 和  Serial Old

年轻代——Serial垃圾回收器

serial垃圾回收器是一种单线程串行回收年轻代的垃圾回收器。

当进行垃圾回收的时候,会停止所有用户线程。

  • 回收年代:年轻代
  • 算法:复制算法
  • 优点:单CPU处理器下吞吐量非常出色
  • 缺点:多CPU吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
  • 适用场景:Java编写的客户端程序或者硬件配置有限的场景

老年代——Serial垃圾回收器

Serial Old是Serial垃圾回收器的老年代版本,采用单线程串行回收。

当进行垃圾回收的时候,会停止所有用户线程。

  • 回收年代:老年代
  • 算法:标记-整理算法
  • 优点:单CPU处理器下吞吐量非常出色
  • 缺点:多CPU吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
  • 适用场景:与Serial垃圾回收器搭配使用或者在CMS特殊情况下使用

垃圾回收器组合——ParNew和CMS

年轻代——ParNew垃圾回收器

ParNew垃圾回收器本质是对Serial在多CPU下的优化,使用多线程进行垃圾回收

当进行垃圾回收的时候,会使用多线程进行回收,这是与Serial最大的不用。

  • 回收年代:年轻代
  • 算法:复制算法
  • 优点:多CPU处理器下停顿时间较短
  • 缺点:吞吐量和停顿时间不如G1
  • 适用场景:JDK8之前的版本中,与CMS老年代垃圾回收器搭配使用

老年代——CMS垃圾回收器

CMS(Concurrent Mak)垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。

执行步骤:

  1. 初始标记:用极短的时间标记出GC Root能直接关联的对象。(可达性分析算法标记)
  2. 并发标记:标记所有的对象(可回收和不可回收),用户线程不需要停止。
  3. 重新标记:由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
  4. 并发清除:清理死亡的对象,用户线程不需要暂停。
  • 回收年代:老年代
  • 算法:标记清除算法
  • 优点:系统由于垃圾回收出现的停顿时间较短,用户体验好
  • 缺点:内存碎片化、退化问题、浮动垃圾问题。
  • 适用场景:大型 互联网系统中用户请求数据量大、频率高的场景。比如订单接口、商品接口等。

CMS垃圾回收器存在的问题

  1. CMS使用了标记-清除算法,在垃圾回收结束之后会出现大量的内存碎片,CMS会在Full GC时进行碎片化的整理。
  2. 无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收。
  3. 如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。

垃圾回收器组合——Parallel Scavenge和Parallel Old

年轻代——Parallel Scavenge垃圾回收器

Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。

  • 回收年代:年轻代
  • 算法:复制算法
  • 优点:吞吐量高,而且手动可控,为了提高吞吐量,虚拟机会动态调整堆的参数
  • 缺点:不能保证单次的停顿时间
  • 适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象。比如:大数据的处理,大文件导出

老年代——Parallel Old垃圾回收器

 Parallel Old是为了Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。

  • 回收年代:老年代
  • 算法:标记-整理算法
  • 优点:并发收集,在多核CPU下效率较高
  • 缺点:暂停时间会比较长
  • 适用场景:与Parallel Scavenge配套使用

G1垃圾回收器

JDK9之后默认的垃圾回收器G1(Garbage First)垃圾回收器

Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小。

CMS关注暂停时间,但是吞吐量方面会下降。

而G1设计目标就是将上述两种垃圾回收器的优点融合:

  1. 支持巨大的堆空间回收,并由较高的吞吐量。
  2. 支持多CPU并行垃圾回收。
  3. 允许用户设置最大暂停时间。

G1的整个堆会被划分成多个大小相等的区域,称之为Region,区域不要求是连续的,分为Eden,Survivor、Old区。Region的大小通过堆空间大小/2048计算得到,也可以通过参数指定大小,Region size必须是2的指数幂,取值范围从1M到32M。

G1垃圾回收方式;

  1. 年轻代回收(Young GC):回收Eden区和Survivor区不用的对象。会导致STW,G1中可以通过参数调整每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。
  2. 混合回收(Mixed GC)

G1垃圾回收器执行流程

  1. 新创建的对象会存放在Eden区。当G1判断年轻代不足(年轻代设置大小的60%),无法分配对象时需要回收时会执行Young GC。
  2. 标记出Eden和Survivor区域中的存活对象。
  3. 根据配置的最大暂停时间选择某些区域将存放对象复制到一个新的Survivor区中(年龄加1),清空这些区域。
  4. 后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
  5. 当某个对象年龄达到阈值时,将会被放入老年代。
  6. 部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。
  7. 多次回收之后,会出现很多老年代区,此时总堆占有率达到阈值,会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象及大对象区。(复制算法完成)

G1垃圾回收器-混合回收

  • 混合回收分为:初始标记、并发标记、最终标记、并发清理

  • G1堆老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收率最高

G1回收器-Full GC

如果清理过程中发现没人足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停,所以尽量保证应该用的堆内存有一定多余的空间。

G1垃圾回收器-Garbage First垃圾回收器

  • 回收年代:老年代和年轻代
  • 算法:复制算法
  • 优点:对比较大的堆如超过6G的堆回收时,延迟可控;不会产生内存碎片;并发标记的SATB算法效率高
  • 缺点:JDK8之前还不够成熟
  • 适用场景:JDK8最新版本、JDK9之后建议默认使用

相关文章:

  • 轻量级碎片化笔记memos本地NAS部署与跨平台跨网络同步笔记实战
  • 蓝桥杯 - 中等 - 健身大调查
  • 软考-软件设计师学习总结-存储系统
  • 3. 列表操作
  • JavaScript浅拷贝与深拷贝
  • 从理论到实战:深度解析MCP模型上下文协议的应用与实践
  • WSA(Windows Subsystem for Android)安装LSPosed和应用教程
  • git 提交空文件夹
  • Multi Agents Collaboration OS:数据与知识协同构建数据工作流自动化
  • C# 看门狗策略实现
  • JavaScript:游戏开发的利器
  • LangChain-输出解析器 (Output Parsers)
  • Python设计模式:命令模式
  • c++自学笔记——字符串与指针
  • Android 手机指纹传感器无法工作,如何恢复数据?
  • 四旋翼无人机手动模式
  • 在kotlin的安卓项目中使用dagger
  • 【CompletableFuture】异步编程
  • 每天五分钟玩转深度学习PyTorch:搭建LSTM算法模型完成词性标注
  • 使用libcurl编写爬虫程序指南