Java进阶—GC回收(垃圾回收)

1. 什么是垃圾回收

垃圾回收(Garbage Collection,GC)是Java虚拟机(JVM)的一项重要功能,用于自动管理程序中不再使用的内存。在Java中,程序员不需要手动释放内存,因为GC会自动检测并回收不再使用的对象,从而减少内存泄漏的风险。

2. 垃圾回收的空间

Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 堆内存中对象的分配与回收。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。

从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。

记住jdk8中的垃圾回收区域就好

3. 垃圾回收器的分类

3.1按实现方式分类

  1. 串行垃圾回收器(Serial GC):使用单个线程进行垃圾回收,适用于单核CPU环境。可以通过 -XX:+UseSerialGC 参数启用。
  2. 并行垃圾回收器(Paraller GC):使用多个线程同时进行垃圾回收,提高回收效率,适用于多核CPU环境。可以通过 -XX:+UseParallelGC 参数启用。
  3. 并发垃圾回收器(Concurrent GC):在应用程序运行的同事进行垃圾回收,减少停顿时间,提高响应性能。可以通过 -XX:+UseConcMarkSweepGC 参数启用。
  4. G1垃圾回收器(Garbage-First Garbage Collector):一种面向服务端应用的垃圾回收器,具有高吞吐量和地停顿时间的特点,适用于大内存应用。可以通过 -XX:+UseG1GC 参数启用。
  5. ZGC:一种低延迟的垃圾回收器,适用于需要更短停顿时间的应用场景。可以通过 -XX:+UseZGC 参数启用。

Java虚拟机的默认垃圾回收器随着Java版本和虚拟机的不同而有所变化。一般来说,在较早的Java版本中(如Java 8及之前的版本),默认的垃圾回收器是串行垃圾回收器(Serial GC),适用于单核CPU环境。

从Java 9开始,默认的垃圾回收器是G1垃圾回收器(Garbage-First Garbage Collector)。G1垃圾回收器是一种面向服务端应用的垃圾回收器,具有高吞吐量和低停顿时间的特点,适用于大内存应用。

3.2 按作用范围分类

  1. 新生代垃圾收集(Minor GC/Young GC):只对新生代进行垃圾回收。新生代一般使用复杂算法进行收集,将存货的对象复制到另一个区域,并清理掉原区域中的无用对象。因为新生代的对象生命周期较短,所以新生代的垃圾回收频率比较高。
  2. 老年代垃圾收集Major GC/Old GC):只对老年代进行垃圾收集。老年代一般使用标记-清除(Mark and Sweep)或者标记-整理(Mark and Compact)算法进行收集,清理掉不再使用的对象。老年代的垃圾回收相对较少,但由于老年代中存放着长期存活的对象,因此垃圾收集的效率和停顿时间会影响到整个应用的性能。
  3. 混合收集(Mixed GC):对整个新生代和部分老年代收集。混合收集通常是为了提高垃圾收集的效率,将部分老年代中的对象以一并清理掉,减少老年代的内存占用。
  4. 整堆收集(Full GC):收集整个Java堆和方法区。在有些语境中,Major GC也可以用来指代整堆收集。整堆收集通常发生在老年代空间不足或元空间(在Java 8及之后的版本中)空间不足时,或者在执行显式的System.gc()方法时。

这些不同的垃圾收集类型在不同的情况下会被JVM自动触发,以维护Java应用程序的内存使用和性能。

- 下面是触发不同类型垃圾回收的一些情况:

  1. 新生代收集(Minor GC / Young GC)
    • 当新生代中的Eden区满时,触发Minor GC。在Minor GC中,会将存活的对象复制到Survivor区,并清理掉Eden区和其中的无用对象。
    • 当Survivor区无法容纳所有存活的对象时,存活较长时间的对象会被移动到老年代,而不是进行Minor GC。
  2. 老年代收集(Major GC / Old GC)
    • 当老年代的空间不足以存放新分配的对象时,会触发Major GC。Major GC会清理老年代中的无用对象,以释放空间给新的对象使用。
    • 在并发标记-清除算法中,当老年代的内存使用达到一定阈值时,会触发并发标记,然后在空闲时进行垃圾回收。
  3. 混合收集(Mixed GC)
    • 混合收集通常在老年代垃圾回收的同时,对新生代也进行部分回收。这种方式可以更均衡地处理新生代和老年代的内存回收,提高整体性能。
  4. 整堆收集(Full GC)
    • 在老年代空间不足时,或者在永久代(在Java 8之前的版本中)或元空间(在Java 8及之后的版本中)空间不足时,会触发整堆收集。
    • 显式调用System.gc()方法时,也可能触发整堆收集,但并不保证一定会执行。

4. 堆中对象的生命周期

  1. 加载阶段:当程序使用new关键字创建一个对象时,该对象会被加载堆内存中。它通常会被分配到新生代的Eden区,有一种特殊情况,大对象会直接分配到老年区。

大对象(LargeObject)通常会直接分配到老年代。在Java中,大对象是指占用内存较大的对象,例如大数组或大集合。由于大对象占用的内存较大,将其分配到新生代可能会导致频繁的内存复制和回收,影响程序的性能。

G1 垃圾回收器会根据 -XX:G1HeapRegionSize 参数设置的堆区域大小和-XX:G1MixedGCLiveThresholdPercent 参数设置的阈值,来决定哪些对象会直接进入老年代。

Parallel Scavenge垃圾回收器中,默认情况下,并没有一个固定的阈值(XX:ThresholdTolerance是动态调整的)来决定何时直接在老年代分配大对象。而是由虚拟机根据当前的堆内存情况和历史数据动态决定。

  1. 存活阶段 :在对象被加载到堆内存后,如果该对象仍然被引用着,则认为该对象处于存活状态。如果对象不再被任何引用引用,则认为该对象是垃圾,将在下一次垃圾回收时被回收。
    如果对象在新生代经过一次 Minor GC后仍然存活,它将被移动到新生代的Survivor区。在Survivor区中经过多次存活后,对象可能被晋升到老年代。
    • 怎么判断对象进入老年代
      • 年龄判断:在新生代中,每个对象被创建时都会被赋予一个年龄计数器。经过一次Minor GC,如果对象仍然存活,它的年龄就会增加。当对象的年龄达到一定阈值时(通常是15),就会晋升到老年代。
      • Survivor空间不足:Survivor空间用来存放新生代中的存活对象。如果Survivor空间不足以容纳对象,那么这些存活对象会被直接晋升到老年代。
      • 空间分配担保:在进行Minor GC时,如果新生代中的对象无法全部晋升到老年代,但是老年代的剩余空间不足以存放新生代中的所有存活对象时,JVM会进行一次Full GC,确保能够为新生代中的对象分配足够的空间。

3.垃圾回收阶段:当对象不再被引用时,JVM的垃圾回收器会识别并回收这些对象所占用的内存。垃圾回收的具体时间取决于垃圾回收器的策略和堆的使用情况。

  • 怎么判断对象可以被回收
    • 引用计数器:引用计数器是一种简单的方法,它通过在对象上维护一个引用计数器来记录对象被引用的次数。当引用计数器为0时,表示对象不再被引用,可以被回收。然而,引用计数器无法处理循环引用的情况,因此在Java中并没有使用这种方法。

    • 可达性分析:Java使用可达性分析来判断对象是否不再被引用。可达性分析是从一组称为"GC Roots"的根对象开始,递归地遍历所有对象的引用关系,标记所有被引用的对象为存活对象,未被标记的对象则被认为是垃圾。这样,不再被引用的对象最终会被垃圾收集器回收。

    • 在Java中,GC Roots就是根集合,包括:

      • 虚拟机栈(Java Stack)中的引用对象,即局部变量和参数。
      • 方法区(Method Area)中的类静态属性引用的对象。
      • 方法区中常量引用的对象。
      • 本地方法栈(Native Method Stack)中JNI(Java Native Interface)引用的对象。
    • 如何判断一个常量是废弃常量?

      假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。

    • 如何判断一个类是无用的类?

      正常很难满足这三个条件的。

      • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
      • 加载该类的 ClassLoader 已经被回收。
      • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  1. 终结阶段:在对象被回收之前,JVM会调用对象的finalize()方法进行清理和释放资源。但是,finalize()方法并不保证一定会被调用,因此不应该依赖于finalize()方法来释放资源。finalize()的调用时机是不确定的,不能保证一定会被调用
  2. 内存回收阶段:在垃圾回收阶段,如果对象经过finalize()方法后仍然未被引用,则会被回收,并释放其占用的内存空间。对于新生代的对象,会经过Minor GC;对于老年代的对象,会经过Major GC。

5. 对象的引用对垃圾回收的影响

  • 强引用、软引用、弱引用、虚引用
    在Java中,引用(Reference)是用来描述对象之间的关系的。Java提供了几种不同类型的引用,包括强引用、软引用、弱引用和虚引用,它们主要用于控制对象的可达性,从而影响垃圾回收的行为。
    1. 强引用(Strong Reference):强引用是最常见的引用类型。如果一个对象具有强引用,即使内存空间不足,垃圾收集器也不会回收这个对象。例如:

      java 复制代码
      Object obj = new Object(); // obj是一个强引用
    2. 软引用(Soft Reference):软引用用于描述那些还有用但并非必须的对象。在系统内存不足时,垃圾收集器会根据软引用的情况来决定是否回收对象。软引用可以通过java.lang.ref.SoftReference类来创建:

      java 复制代码
      SoftReference<Object> softRef = new SoftReference<>(new Object());

      例如,缓存中的对象可以使用软引用,当内存不足时,垃圾回收器可以根据软引用情况来回收缓存中的对象,从而释放内存。

    3. 弱引用(Weak Reference):弱引用比软引用更弱,只要垃圾回收器运行,无论内存是否足够,都会回收只被弱引用指向的对象。弱引用可以通过java.lang.ref.WeakReference类来创建:

      java 复制代码
      WeakReference<Object> weakRef = new WeakReference<>(new Object());

      弱引用通常用于实现一些缓存中的临时对象,可以随时被回收而不会占用太多内存。

    4. 虚引用(Phantom Reference):虚引用是所有引用中最弱的一种。一个持有虚引用的对象,和没有引用几乎是一样的,随时可能被垃圾回收器回收。虚引用主要用于在对象被回收时收到一个系统通知或执行一些清理操作。虚引用可以通过java.lang.ref.PhantomReference类来创建:

      java 复制代码
      ReferenceQueue<Object> queue = new ReferenceQueue<>();
      PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);

      虚引用本身并不能阻止对象被回收,它的主要作用是在对象被回收时执行一些特定的操作,例如清理对象关联的资源或发送通知。

      以下是一个简单的示例,演示了如何使用虚引用和引用队列:

      java 复制代码
      import java.lang.ref.PhantomReference;
      import java.lang.ref.Reference;
      import java.lang.ref.ReferenceQueue;
      
      public class PhantomReferenceExample {
          public static void main(String[] args) {
              Object obj = new Object();
              ReferenceQueue<Object> queue = new ReferenceQueue<>();
              PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
      
              // 显示判断对象是否被回收
              System.out.println("Is object still alive? " + (phantomRef.get() != null));
      
              // 释放对象引用
              obj = null;
      
              // 强制垃圾回收
              System.gc();
      
              // 检查引用队列是否有引用对象
              Reference<?> refFromQueue = queue.poll();
              if (refFromQueue != null) {
                  System.out.println("Object is in the queue.");
              } else {
                  System.out.println("Object is not in the queue.");
              }
          }
      }

      在这个例子中,当对象被垃圾回收器回收时,phantomRef将会被添加到queue引用队列中,我们可以通过检查引用队列中是否有引用来判断对象是否被回收。

这些引用类型可以帮助开发人员更灵活地控制对象的生命周期和内存回收行为,特别是在处理大量数据或需要特定内存管理策略的情况下。

6. 垃圾回收算法

  1. 标记-清除(Mark and Sweep)算法

    标记-清除算法(Mark and Sweep Algorithm)是一种基本的垃圾回收算法,用于标记和清除不再被引用的对象。该算法分为两个阶段:标记阶段和清除阶段。

    • 标记阶段
      • 从根对象(如虚拟机栈中的引用对象、静态变量等)开始,遍历所有可以访问到的对象,并标记这些对象为活动对象。
      • 对于无法访问到的对象,即不可达对象,将其标记为待清除对象。
    • 清除阶段
      • 遍历堆内存中的所有对象,将标记为待清除的对象进行清除操作,即回收其所占用的内存。

    标记-清除算法的优点是简单高效,但也存在一些缺点:

    • 内存碎片问题:由于标记-清除算法会在清除阶段产生不连续的内存碎片,可能导致无法找到足够大的连续内存块来分配新对象。
    • 效率问题:标记和清除过程可能会耗费较长的时间,且在清除阶段会暂停应用程序的运行。
  2. 复制算法(Copying Algorithm)

    复制算法(Copying Algorithm)是一种垃圾回收算法,通常用于新生代的内存管理。它的核心思想是将内存空间分为两个大小相等的区域,通常称为"From"区和"To"区。在垃圾回收过程中,所有存活的对象都会被复制到"To"区,而非存活的对象则会被丢弃。

    复制算法的具体步骤如下:

    1. 初始分配:将新生代内存空间分为两个大小相等的区域,通常称为"From"区和"To"区。
    2. 新创建的对象首先会被分配到"From"区。
    3. 标记存活对象:从根对象开始,通过可达性分析标记所有存活的对象。
    4. 复制存活对象:将所有存活的对象复制到"To"区,同时按照对象的地址顺序依次排列,确保对象之间的地址是连续的。
    5. 更新引用:更新所有指向存活对象的引用,使其指向"To"区中的地址。
    6. 交换空间:将"From"区和"To"区的角色互换,使得下一次垃圾回收时仍然使用这两个区域,而不需要重新分配内存空间。

    复制算法的优点是简单高效,并且可以解决标记-清除算法中的内存碎片问题。但是,它也有一些缺点,主要是需要额外的内存空间来存储复制后的对象,以及在复制过程中需要暂停应用程序的运行。为了减少这些缺点,通常会将新生代划分为更多的存活区域,并使用"对象年龄"来决定何时将对象晋升到老年代。

    总结一下:
    可用内存变小 :可用内存缩小为原来的一半。
    不适合老年代:如果存活对象数量比较大,复制性能会变得很差。

  3. 标记-整理算法(Mark-Sweep-Compact Algorithm)

    标记-整理算法(Mark-Sweep-Compact Algorithm)是一种垃圾回收算法,通常用于老年代的内存管理。它结合了标记-清除算法和内存整理的思想,用于解决标记-清除算法可能产生的内存碎片问题。

    标记-整理算法的主要步骤包括:

    1. 标记阶段:从根对象开始,通过可达性分析标记所有存活的对象。
    2. 清除阶段:遍历整个堆内存,将未被标记的对象清除,即回收其占用的内存空间。
    3. 整理阶段:将存活的对象向一端移动,使得所有存活对象在内存中连续排列,从而将内存空间合并为一个大的连续空间。

    标记-整理算法的优点是可以避免内存碎片问题,使得堆内存中的空闲空间更加连续,有利于提高内存分配的效率。但是,与复制算法相比,标记-整理算法需要更多的计算和移动操作,可能会影响应用程序的性能。因此,通常只在老年代等内存碎片严重的情况下使用。

相关推荐
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
小白学大数据3 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
冰芒猓4 小时前
SpringMVC数据校验、数据格式化处理、国际化设置
开发语言·maven
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet