JVM内存和垃圾回收-13.垃圾回收算法

什么是垃圾?

  • 在运行程序中没有任何指针指向的对象就是需要被回收的垃圾

  • 因为垃圾对象占用的内存会一直保持到程序结束,所以垃圾较多会导致内存溢出

为什么需要GC?

  • 不进行GC,内存会被一直不停消耗

  • 清理内存中的碎片,将整理出的内存分配给新对象

  • GC能保证程序的正常运行

Java垃圾回收机制

垃圾收集器针对堆(频繁收集年轻代,较少收集老年代,基本不收集永久代/元空间)和方法区(不是所以JVM都会对其进行回收)


1.垃圾标记阶段

进行GC前需要区分内存中哪些对象存活,哪些已经死亡

1.1 引用计数器算法
  • 基本思路:每个对象保持一个整型的引用计数器属性,有对象引用该对象则计数器加1,引用失效就减1。当计数器值为0时,该对象可被回收

  • 优点:

    • 实现简单
    • 判定效率高,回收没有延迟
  • 缺点:

    • 增加空间开销(需要存储计数器)
    • 增加时间开销(更新计数器的时间)
    • 无法处理循环引用,导致无法回收进而导致内存泄漏(只能手动GC),垃圾回收器不会使用该算法

    在这里插入图片描述

1.2 可达性分析算法
  • 基本思路:

    • 以GC Roots为起始点(一组必须活跃的引用),从上到下搜索被根对象集合连接的目标对象是否可达
    • 被根对象直接/间接连接,则视为存活对象,它们之间的路径称为引用链
    • 目标对象没有任何引用链则不可达,会被标记为垃圾对象

    在这里插入图片描述

  • GC Roots包括对象:

    • 虚拟机栈引用的对象,比如方法中使用到的参数、局部变量等
    • 本地方法栈中本地方法引用的对象
    • 方法区中类的静态属性引用的对象
    • 方法区中常量引用的对象,比如字符串常量池中的引用

可达性分析需要保证在快照中进行(为了保证分析结果的准确性),所以GC需要Stop The World(即使是CMS收集器,枚举根节点也需要停顿)


2.对象的finalization机制

Java提供finalization机制允许开发人员提供对象被销毁前的自定义处理逻辑

  • 当垃圾回收对象前,垃圾回收机制会先调用该对象的finalize方法

  • finalize方法可被子类重写,用于在对象被回收时进行资源释放

  • 不要主动调用对象的finalize方法,原因如下:

    • 可能会导致对象复活
    • 执行时间没有保障(该方法由GC线程决定,如果不GC,该方法调用了也不会被执行)
    • 较差的finalize方法会影响GC性能(比如方法存在死循环)
  • finalize方法的存在会导致对象处于三种状态:

    • 可触及的:从GC Roots节点开始可达到的对象
    • 可复活的:对象的所有引用都被释放,但可能在finalize方法复活
    • 不可触及的:对象的finalize方法被调用但没有复活(处于该状态的对象不可能被复活,因为finalize方法只能被调用一次),此时才可被回收
  • 由于存在finalize方法,所以对象即使不可达也不一定会被回收。因此判断对象A能否被回收,至少需经历两次标记过程:

    • 对象A到GC Roots没有引用链,则进行第一次标记(无法立马判定是否能被回收)
    • 判断对象A是否有必要执行finalize方法:
      • 如果对象A没有重写finalize方法或者方法已被JVM调用过,则对象A被判定为不可触及的
      • 如果对象A重写了finalize方法且还未被执行,则对象A被插入到队列中
    • GC会对队列中的对象A进行第二次标记:
      • 如果在finalize方法中对象A与引用链上其他对象建立了联系,则对象A不会被回收。但之后再次出现未被引用的情况,由于finalize方法只会被调用一次,对象A会直接被回收
      • 没有建立联系则被回收

3.垃圾清除阶段

区分出内存中存活对象和死亡对象后,GC就需要执行垃圾回收,释放无用对象占用的内存

在这里插入图片描述

3.1 标记清除算法
  • 执行过程:当堆中的有效内存被耗尽后,会停止程序(stop the world),保证一致性,然后进行两项工作
    • 标记:Collector从GC Roots开始遍历,标记所有被引用的对象(非垃圾)
    • 清除:Collector对堆中内存进行线性遍历,如果发现某个对象的Header中没有标记为可达对象,则将其回收(清除并不是置空,而是将需要清除的对象地址保存在列表中,需要时从通过列表中去找)

在这里插入图片描述

  • 缺点:
    • 效率不高
    • GC是需要停止整个程序,导致用户体验差
    • 清理后的空闲内存不连续,存在内存碎片,需要维护空闲列表标明哪些空间是空闲的
3.2 复制算法
  • 核心思想:
    • 将存活的内存空间分为两块(正在使用和未被使用),每次只使用其中一块
    • GC时将存活对象复制到未被使用的内存块中,然后清除正在使用的内存块中所有对象
    • 交换两个内存的角色(即survivor中的from和to区)

在这里插入图片描述

  • 优点:

    • 没有标记和清除的过程,实现简单,运行高效
    • 复制后的空间是连续的,不会出现内存碎片
  • 缺点:

    • 需要两倍的内存空间(或者称内存利用率减半)
    • 复制一块内存空间后,栈中指向原空间的地址需要指向新空间的地址(即需要维护额外的引用关系),内存占用变大
  • 应用场景:

    • 新生代中每次回收的垃圾较多(意味着存活对象少),需要复制的对象较少,回收性价比高
3.3 标记压缩算法

老年代保存的对象都很大,而标记清除会产生大量内存碎片;老年代对象存活数较多,意味着复制算法需要复制对象较多

  • 执行过程:

    • 第一阶段:和标记清除一样,从根节点开始标记所有被引用对象
    • 第二阶段:标记清除并不会移动对象,而该算法会将存活对象压缩到内存一端,按顺序排放(即内存碎片整理)
    • 清理边界外的所有空间
  • 优点:

    • 消除了标记清除算法中的内存碎片(当需要给新对象分配内存时,JVM只需要持有内存的起始地址即可),不需要维护空闲列表
    • 消除了复制算法中内存利用率减半的代价
  • 缺点:

    • 比复制算法(多了标记阶段)和标记清除算法(多了内存整理阶段)效率低

    • 因为被移动对象被其他对象引用,需要调整引用的地址

    • 移动过程需要停止程序

什么是指针碰撞?

如果内存分布规整(即已使用和未使用的内存各自在一边),彼此间会维护者记录下一次分配起始点的标记指针。为新对象分配内存时,只需要修改指针的偏移量即可,该分配方式称为指针碰撞


4.分代收集算法

  • 不同对象(即新生代和老年代)的存活时间不同,意味着可以采取不同的垃圾回收算法,进而提高回收效率,所以几乎所有GC都采用分代收集算法进行垃圾回收

  • 新生代区域小、对象存活时间短、回收频繁,所以可采用复制算法进行回收(经过内存利用率不高,但是由于survivor中from和to区都比较小,可以接受其中一个区不被利用)

  • 老年代区域大、对象存活时间长、回收不频繁,所以可采用标记压缩算法


5.增量收集算法、分区算法

上述三个算法中都会停止程序,如果垃圾回收时间过长,会影响用户体验

5.1 增量收集算法
  • 基本思想:如果一次处理所有垃圾,会造成长时间停顿。可以让垃圾收集线程每次收集一小块区域,然后再切换到应用程序,这样进行线程来回的切换,直到垃圾回收完毕(本质依旧是标记清除和复制算法)

  • 缺点:

    • 上下文切换造成多大消耗,回收成本上升
    • 系统吞吐量下降
5.2 分区算法
  • 基本思想:堆空间越大意味着每次GC时间越长,停顿越长。为了控制GC时间,将堆划分为多个小区域,根据每个区域回收时的停顿时间,合理回收若干个小区域(因为不是回收整个堆空间,减少了一次GC的停顿)。分代算法依据存活时间划分两个区域(即新生代和老年代)进行回收,分区算法将整个堆空间划分为不同小区间

  • 优点:

    • 每个区间独立回收,可控制一次回收多少个区间

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值