MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java垃圾回收机制与算法

2021-01-205.5k 阅读

Java垃圾回收机制概述

在Java编程中,垃圾回收(Garbage Collection,GC)是自动管理内存的重要机制。Java的设计者们致力于让开发者从繁琐的手动内存管理中解脱出来,避免因手动释放内存不当而导致的内存泄漏和悬空指针等问题。

Java运行时系统(JVM)会定期检查堆内存中不再被使用的对象,并自动回收它们占用的内存空间。垃圾回收的过程对Java开发者来说通常是透明的,开发者只需要专注于业务逻辑的实现,而无需关心对象何时应该被销毁以及内存何时该被释放。

堆内存结构

为了更好地理解垃圾回收机制,首先需要了解Java堆内存的结构。Java堆内存主要分为新生代(Young Generation)和老年代(Old Generation)。新生代又进一步细分为伊甸园区(Eden Space)和两个幸存区(Survivor Space,通常命名为From Survivor和To Survivor)。

  • 伊甸园区(Eden Space):大多数新创建的对象最初会被分配到伊甸园区。当伊甸园区空间满时,会触发一次Minor GC(新生代垃圾回收)。
  • 幸存区(Survivor Space):经过一次Minor GC后,仍然存活的对象会被移动到其中一个幸存区(通常是From Survivor)。在后续的Minor GC中,幸存区的对象年龄会增加,当对象达到一定年龄(默认是15,可通过参数 -XX:MaxTenuringThreshold 调整),会被晋升到老年代。
  • 老年代(Old Generation):存储生命周期较长的对象,当老年代空间不足时,会触发Major GC(也叫Full GC),回收老年代的垃圾对象。

垃圾回收算法

Java垃圾回收机制采用了多种垃圾回收算法,不同的算法适用于不同的场景,下面详细介绍几种常见的垃圾回收算法。

标记 - 清除算法(Mark - Sweep)

  1. 工作原理
    • 标记阶段:从根对象(如栈中的局部变量、静态变量等)开始遍历,标记所有被引用的对象。
    • 清除阶段:遍历整个堆内存,回收所有未被标记的对象,即垃圾对象,并将它们占用的内存空间标记为可用。
  2. 优缺点
    • 优点:实现简单,不需要额外的空间来复制对象。
    • 缺点
      • 碎片化问题:回收后的内存空间是不连续的,容易产生内存碎片,导致后续大对象无法分配到足够的连续内存空间。
      • 效率问题:标记和清除两个阶段都需要遍历整个堆内存,随着堆内存增大,效率会逐渐降低。
  3. 代码示例
public class MarkSweepExample {
    public static void main(String[] args) {
        // 创建一些对象,模拟内存使用
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();

        // 假设obj2不再被引用,成为垃圾对象
        obj2 = null;

        // 这里可以手动触发垃圾回收(但不保证一定会执行)
        System.gc();
    }
}

在上述示例中,obj2 被赋值为 null 后,理论上成为了垃圾对象,当触发垃圾回收时,标记 - 清除算法可能会回收 obj2 占用的内存空间。

复制算法(Copying)

  1. 工作原理
    • 将内存空间划分为两个相等的区域(如在新生代的伊甸园区和一个幸存区)。当其中一个区域(如伊甸园区)满时,触发垃圾回收。
    • 从根对象开始遍历,将存活的对象复制到另一个区域(如幸存区),并保持对象之间的相对顺序。
    • 复制完成后,直接清空原来的区域,该区域的内存空间就可作为新的分配空间使用。
  2. 优缺点
    • 优点
      • 高效:只需要复制存活对象,并且清除操作简单,只需清空原来的区域,避免了碎片化问题。
      • 适合新生代:新生代对象通常存活率较低,复制算法适合这种场景。
    • 缺点
      • 空间浪费:需要两倍的内存空间来实现复制算法,因为始终有一半的空间处于闲置状态。
  3. 代码示例 以下代码虽然不能直接体现复制算法的内部实现,但可以模拟对象在不同区域的移动。
public class CopyingExample {
    private static final int INITIAL_CAPACITY = 10;
    private Object[] eden = new Object[INITIAL_CAPACITY];
    private Object[] survivor = new Object[INITIAL_CAPACITY];
    private int edenIndex = 0;
    private int survivorIndex = 0;

    public void addObject(Object obj) {
        if (edenIndex >= eden.length) {
            // 模拟垃圾回收,复制存活对象到幸存区
            for (int i = 0; i < edenIndex; i++) {
                if (eden[i] != null) {
                    survivor[survivorIndex++] = eden[i];
                }
            }
            // 清空伊甸园区
            edenIndex = 0;
        }
        eden[edenIndex++] = obj;
    }

    public static void main(String[] args) {
        CopyingExample example = new CopyingExample();
        example.addObject(new Object());
        example.addObject(new Object());
        // 模拟一些对象成为垃圾
        example.addObject(null);
        example.addObject(new Object());
        example.addObject(null);
        example.addObject(new Object());
        // 再次添加对象触发垃圾回收模拟
        example.addObject(new Object());
    }
}

在上述代码中,addObject 方法模拟了对象在伊甸园区的添加,当伊甸园区满时,会将存活对象复制到幸存区,并清空伊甸园区,类似于复制算法的工作过程。

标记 - 整理算法(Mark - Compact)

  1. 工作原理
    • 标记阶段:与标记 - 清除算法类似,从根对象开始遍历,标记所有被引用的对象。
    • 整理阶段:将所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存空间,使得内存空间连续。
  2. 优缺点
    • 优点
      • 解决碎片化问题:通过整理存活对象,避免了内存碎片化,提高了内存利用率。
      • 适合老年代:老年代对象存活率较高,标记 - 整理算法相较于复制算法更适合老年代的场景,因为不需要额外的大量空间进行复制。
    • 缺点
      • 效率问题:整理阶段需要移动对象,相对标记 - 清除算法效率较低,特别是在对象数量较多时。
  3. 代码示例
import java.util.ArrayList;
import java.util.List;

public class MarkCompactExample {
    private static class ObjectWrapper {
        private Object obj;
        private boolean isAlive;

        public ObjectWrapper(Object obj) {
            this.obj = obj;
            this.isAlive = true;
        }
    }

    public static void main(String[] args) {
        List<ObjectWrapper> objects = new ArrayList<>();
        objects.add(new ObjectWrapper(new Object()));
        objects.add(new ObjectWrapper(new Object()));
        objects.add(new ObjectWrapper(null));
        objects.add(new ObjectWrapper(new Object()));

        // 标记阶段
        for (ObjectWrapper wrapper : objects) {
            if (wrapper.obj == null) {
                wrapper.isAlive = false;
            }
        }

        // 整理阶段
        int writeIndex = 0;
        for (ObjectWrapper wrapper : objects) {
            if (wrapper.isAlive) {
                objects.set(writeIndex++, wrapper);
            }
        }
        // 清除无用对象
        while (writeIndex < objects.size()) {
            objects.remove(writeIndex);
        }
    }
}

在上述代码中,ObjectWrapper 类用于包装对象并标记其是否存活。通过 isAlive 字段进行标记,然后在整理阶段将存活对象向前移动,最后清除无用对象,模拟了标记 - 整理算法的过程。

垃圾回收器

Java提供了多种垃圾回收器,每种垃圾回收器都基于上述垃圾回收算法进行实现,并且针对不同的应用场景进行了优化。

Serial垃圾回收器

  1. 特点
    • 单线程垃圾回收器,在进行垃圾回收时,会暂停所有应用线程(Stop - the - World,STW)。
    • 采用复制算法处理新生代,标记 - 整理算法处理老年代。
    • 适用于单核环境和对响应时间要求不高的应用场景,因为其实现简单,内存管理开销小。
  2. 启用方式 在JVM启动参数中添加 -XX:+UseSerialGC
  3. 应用场景 例如一些简单的命令行工具或者对内存要求不高且运行在单核环境的小型应用。

Parallel垃圾回收器

  1. 特点
    • 多线程垃圾回收器,同样会触发Stop - the - World。
    • 在新生代和老年代都采用并行回收的方式,即使用多个线程同时进行垃圾回收,以提高垃圾回收的效率。
    • 适用于对吞吐量要求较高的应用场景,通过提高垃圾回收的并行度来减少垃圾回收的总时间,从而提高应用的整体吞吐量。
  2. 启用方式 在JVM启动参数中添加 -XX:+UseParallelGC(新生代并行回收)或 -XX:+UseParallelOldGC(老年代并行回收,Java 6及以后)。
  3. 应用场景 如大数据处理、科学计算等对吞吐量要求较高的应用。

CMS(Concurrent Mark Sweep)垃圾回收器

  1. 特点
    • 以获取最短停顿时间为目标的垃圾回收器。
    • 采用标记 - 清除算法,在垃圾回收过程中,尽量与应用线程并发执行,减少Stop - the - World的时间。
    • 分为初始标记(Initial Mark)、并发标记(Concurrent Mark)、重新标记(Remark)和并发清除(Concurrent Sweep)等阶段。初始标记和重新标记阶段会暂停应用线程,但时间较短,并发标记和并发清除阶段与应用线程并发执行。
  2. 启用方式 在JVM启动参数中添加 -XX:+UseConcMarkSweepGC
  3. 应用场景 适用于对响应时间要求较高的应用,如Web应用程序,用户对响应时间比较敏感,不能长时间忍受应用停顿。

G1(Garbage - First)垃圾回收器

  1. 特点
    • 面向服务端应用的垃圾回收器,旨在取代CMS垃圾回收器。
    • 将堆内存划分为多个大小相等的Region,每个Region可以扮演伊甸园区、幸存区或老年代的角色。
    • 采用标记 - 整理算法,能够预测垃圾回收的停顿时间,并根据停顿时间目标来选择回收的Region,优先回收垃圾最多的Region(Garbage - First的由来)。
    • 同样有初始标记、并发标记、最终标记和筛选回收等阶段,与应用线程并发执行部分操作,减少停顿时间。
  2. 启用方式 在JVM启动参数中添加 -XX:+UseG1GC
  3. 应用场景 适用于大内存、多处理器的服务器环境,对响应时间和吞吐量都有较高要求的应用,如大型电商平台、企业级应用等。

垃圾回收机制的调优

垃圾回收机制的调优对于提高Java应用的性能至关重要。调优的目标通常是减少垃圾回收的频率和停顿时间,提高应用的吞吐量和响应速度。

分析工具

  1. Jconsole:JDK自带的图形化监控工具,可以监控JVM的内存使用情况、线程状态、垃圾回收次数和时间等信息。通过在命令行输入 jconsole 启动。
  2. VisualVM:功能更强大的JDK自带工具,除了基本的监控功能外,还可以进行线程分析、堆转储分析等。在命令行输入 jvisualvm 启动。
  3. YourKit Java Profiler:商业性能分析工具,提供了详细的性能分析功能,包括内存分析、CPU分析等,能帮助开发者快速定位性能瓶颈。

调优策略

  1. 调整堆内存大小:通过 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数来调整堆内存的大小。如果堆内存过小,会导致频繁的垃圾回收;如果堆内存过大,垃圾回收的时间会变长。例如,对于一个Web应用,可以根据预估的并发用户数和数据量来合理调整堆内存大小。
  2. 选择合适的垃圾回收器:根据应用的特点选择合适的垃圾回收器。如对响应时间敏感的Web应用可以选择CMS或G1垃圾回收器;对吞吐量要求较高的大数据处理应用可以选择Parallel垃圾回收器。
  3. 调整垃圾回收器参数:不同的垃圾回收器有各自的可调整参数。例如,对于G1垃圾回收器,可以通过 -XX:MaxGCPauseMillis 参数设置最大停顿时间目标,G1会尽量在这个时间内完成垃圾回收。

代码示例与调优实践

import java.util.ArrayList;
import java.util.List;

public class GCTuningExample {
    private static final int OBJECT_SIZE = 1024 * 1024; // 1MB

    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            byte[] data = new byte[OBJECT_SIZE];
            list.add(data);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在上述代码中,不断创建1MB大小的字节数组并添加到列表中,模拟内存不断增长的情况。

  1. 使用Jconsole分析 启动应用后,打开Jconsole,连接到运行的Java进程。在“内存”选项卡中,可以看到堆内存的使用情况和垃圾回收的次数、时间等信息。随着程序的运行,会发现堆内存不断增长,垃圾回收次数也会增加。
  2. 调优实践
    • 调整堆内存大小:可以通过在启动参数中设置 -Xms256m -Xmx512m 来调整堆内存的初始大小和最大大小,观察垃圾回收频率和应用性能的变化。
    • 选择垃圾回收器:如果当前使用的是默认垃圾回收器,可以尝试切换到G1垃圾回收器,通过在启动参数中添加 -XX:+UseG1GC,再次运行应用,对比垃圾回收停顿时间和吞吐量的变化。

通过上述调优实践,可以根据应用的实际需求,不断调整参数,找到最优的垃圾回收配置,提高应用的性能。

总之,深入理解Java垃圾回收机制与算法,合理选择垃圾回收器并进行调优,对于开发高效、稳定的Java应用至关重要。开发者需要根据应用的特点和运行环境,灵活运用相关知识和工具,以实现最佳的性能表现。