【JAVA】垃圾回收详解


垃圾回收

垃圾回收,就是释放垃圾占用的空间,从而提升程序性能,防止内存泄露。当一个对象不再被需要时,该对象就需要被回收并释放空间。

Java 内存运行时数据区域包括程序计数器、虚拟机栈、本地方法栈、堆等区域。其中,程序计数器、虚拟机栈和本地方法栈都是线程私有的,当线程结束时,这些区域的生命周期也结束了,因此不需要过多考虑回收的问题。而是虚拟机管理的内存中最大的一块,堆中的内存的分配和回收是动态的,垃圾回收主要关注的是堆空间。


调用垃圾回收器的方法

调用垃圾回收器的方法是 gc ,该方法在 System 类和 Runtime 类中都存在。

java.lang.System.gc 等价于 java.lang.Runtime.getRuntime.gc 的简写,都是调用垃圾回收器。

Runtime 类中,方法 gc 是实例方法,方法 System.gc 是调用该方法的一种传统而便捷的方法。

System 类中,方法 gc 是静态方法,该方法会调用 Runtime 类中的 gc 方法。

方法 gc 的作用是提示 Java 虚拟机进行垃圾回收,该方法由系统自动调用,不需要人为调用。该方法被调用之后,由 Java 虚拟机决定是立即回收还是延迟回收


finalize 方法

与垃圾回收有关的另一个方法是 finalize 方法。该方法在 Object 类中被定义,在释放对象占用的内存之前会调用该方法。

该方法的默认实现不做任何事,如果必要,子类应该重写该方法,一般建议在该方法中释放对象持有的资源。


判断对象是否可回收

垃圾回收器在对堆进行回收之前,首先需要确定哪些对象是可回收的。常用的算法有两种,引用计数算法根搜索算法


引用计数算法

引用计数算法给每个对象添加引用计数器,用于记录对象被引用的计数,引用计数为 0 的对象即为可回收的对象。

虽然引用计数算法的实现简单,判定效率也很高,但是引用计数算法无法解决对象之间循环引用的情况。如果多个对象之间存在循环引用,则这些对象的引用计数永远不为 0,无法被回收。因此 Java 语言没有使用引用计数算法。


根搜索算法

主流的商用程序语言都是使用根搜索算法判断对象是否可回收。根搜索算法的思路是,从若干被称为 GC Roots 的对象开始进行搜索,不能到达的对象即为可回收的对象。

在 Java 中,GC Roots 一般包含下面几种对象:

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

引用的分类

引用计数算法和根搜索算法都需要通过判断引用的方式判断对象是否可回收。

JDK 1.2 之后,Java 将引用分成四种,按照引用强度从高到低的顺序依次是:强引用、软引用、弱引用、虚引用。

  • 强引用:在程序代码中普遍存在的引用。垃圾回收器永远不会回收被强引用关联的对象。
  • 软引用:还有用但并非必需的对象。只有在系统将要发生内存溢出异常时,被软引用关联的对象才会被回收。在 JDK 1.2 之后,提供了 SoftReference 类实现软引用。
  • 弱引用:非必需的对象,其强度低于软引用。被弱引用关联的对象只能存活到下一次垃圾回收发生之前,当垃圾回收器工作时,被弱引用关联的对象一定会被回收。在 JDK 1.2 之后,提供了 WeakReference 类实现弱引用。
  • 虚引用:最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类实现虚引用。

垃圾回收算法

标记—清除算法

标记—清除算法是最基础的垃圾回收算法,后续的垃圾收集算法都是基于标记—清除算法进行改进而得到的。标记—清除算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记—清除算法有两个主要缺点:

  • 效率问题:标记和清除的效率都不高
  • 空间问题:标记清除之后会产生大量不连续的内存碎片,导致程序在之后的运行过程中无法为较大对象找到足够的连续内存。

标记—整理算法

标记—整理算法是根据老年代的特点提出的。标记过程与标记—清除算法一样,但后续步骤不是直接回收被标记的对象,而是让所有存活的对象都向一端移动,然后清除边界以外的内存。


复制算法

复制算法的将可用内存分成大小相等的两块,每次只使用其中的一块,当用完一块内存时,将还存活着的对象复制到另外一块内存,然后把已使用过的内存空间一次清理掉。

复制算法解决了效率问题。由于每次都是对整个半区进行内存回收,因此在内存分配时不需要考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。复制算法的优点是实现简单,运行高效,缺点是将内存缩小为了原来的一半,以及在对象存活率较高时复制操作的次数较多,导致效率降低。


分代收集算法

分代收集算法根据对象的存活周期不同将内存划分为多个区域,对每个区域选用不同的垃圾回收算法。

一般把 Java 堆分为新生代和老年代

  • 在新生代中,大多数对象的生命周期都很短,因此选用复制算法。
  • 在老年代中,对象存活率高,因此选用标记—清除算法或标记—整理算法。

分配内存与回收策略

Java 堆可以分成新生代和老年代,新生代又可以细分成 Eden 区、From Survivor 区、To Survivor 区等。对于不同的对象,有相应的内存分配规则。


Minor GCFull GC

Minor GC 指发生在新生代的垃圾回收操作。因为大多数对象的生命周期都很短,因此 Minor GC频繁执行,一般回收速度也比较快

Full GC 也称 Major GC ,指发生在老年代的垃圾回收操作。出现了 Full GC ,经常会伴随至少依次的 Minor GC 。老年代对象的存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 很多。


对象的区分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。长期存活的对象进入老年代

大对象直接进入老年代。大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。大对象对于虚拟机的内存分配而言是坏消息,经常出现大对象会导致内存还有不少空间时就提前触发垃圾回收以获取足够的连续空间分配给大对象。

将大对象直接在老年代中分配的目的是避免在 Eden 区和 Survivor 区之间出现大量内存复制。


动态对象年龄判定

虚拟机采用分代收集的思想管理内存,因此需要识别每个对象应该放在新生代还是老年代。虚拟机给每个对象定义了年龄计数器,对象在 Eden 区出生之后,如果经过第一次 Minor GC 之后仍然存活,将进入 Survivor 区,同时对象年龄变为 1,对象在 Survivor 区每经过一次 Minor GC 且存活,年龄就增加 1 ,增加到一定阈值时则进入老年代(阈值默认为 15 )。

为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到阈值才能进入老年代。如果在 Survivor 区中相同年龄的所有对象的空间总和大于 Survivor 区空间的一半,则年龄大于或等于该年龄的对象直接进入老年代


空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间总和,如果这个条件成立,那么 Minor GC 可以确保是安全的。

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC ,否则将进行 Full GC


来源:力扣(LeetCode)
链接:https://leetcode-cn.com/leetbook/read/java-interview-highlights/en7epe/

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值