JVM 面试必备知识:垃圾回收背后的那些事儿

举报
bug菌 发表于 2024/11/28 23:24:51 2024/11/28
【摘要】 前言 🌟嗨,亲爱的 Java 爱好者!是不是觉得垃圾回收(GC)只是一个“看似简单、实际很复杂”的东西?你是不是也曾因为内存溢出而痛苦过,也或许在调优 JVM 时为 GC 频繁而烦恼过?没关系,今天我们就来深度解析 Java 虚拟机(JVM)中的垃圾回收机制,揭开它的神秘面纱,帮助你在面试中轻松应对这类问题!你会发现,GC 其实不仅仅是一个 “自动化清理垃圾” 的小功能,它背后有着极其复...

前言 🌟

嗨,亲爱的 Java 爱好者!是不是觉得垃圾回收(GC)只是一个“看似简单、实际很复杂”的东西?你是不是也曾因为内存溢出而痛苦过,也或许在调优 JVM 时为 GC 频繁而烦恼过?没关系,今天我们就来深度解析 Java 虚拟机(JVM)中的垃圾回收机制,揭开它的神秘面纱,帮助你在面试中轻松应对这类问题!你会发现,GC 其实不仅仅是一个 “自动化清理垃圾” 的小功能,它背后有着极其复杂的算法和精妙的设计,掌握它们可以让你在面试中脱颖而出,成为 JVM 领域的小达人!

垃圾回收不仅仅是为了清理内存中的“垃圾”,它的背后藏着许多性能优化的技巧和策略。Java 语言的开发者,尤其是在大型项目和高性能系统中,必须理解垃圾回收的机制,合理地调优它,才能让系统高效、稳定地运行。废话不多说,咱们现在就开始吧!🚀

1. 什么是垃圾回收(GC)?🧹

我们首先需要了解什么是垃圾回收。简单来说,垃圾回收是 Java 的内存管理机制之一,它能够自动回收不再使用的对象所占用的内存,避免了开发者手动管理内存的麻烦。这就好像是生活中的“清理工”,每天默默无闻地清理你不需要的东西,让你可以专心做自己的事情。

垃圾回收的核心思想是通过不断地检查和回收堆内存中的垃圾对象,从而让系统能够自动管理内存。垃圾回收器会寻找那些不再被任何活动线程引用的对象,并回收它们占用的内存空间。Java 内存分为堆内存(Heap)和栈内存(Stack),而垃圾回收主要关注堆内存。

为什么要有垃圾回收?

想象一下,如果没有垃圾回收,程序中的每个对象都需要由开发者手动释放内存。那该有多麻烦!不仅如此,内存泄漏也成了不可避免的难题。所以,JVM 通过垃圾回收机制,自动管理对象生命周期,极大地降低了开发者的负担。

2. 垃圾回收的基本流程 🌱

垃圾回收的基本流程看似简单,但实际运作时,JVM 需要做很多复杂的事情。通常,垃圾回收分为以下几个阶段:

  • 标记阶段(Marking):垃圾回收器会遍历所有对象,标记出那些当前还被引用的对象。
  • 清除阶段(Sweeping):垃圾回收器会删除那些没有被标记的对象,也就是那些不再被引用的对象。
  • 压缩阶段(Compact):某些回收器(如标记-整理算法)会将存活的对象移到堆的一端,从而避免内存碎片问题。

在这个过程中,堆内存中的对象会经历标记、清除、整理等步骤,确保垃圾对象能够被清理掉,并释放出内存空间。

3. 常见的垃圾回收算法 🧑‍🔧

JVM 提供了多种垃圾回收算法,以应对不同的内存管理需求。每种算法都有不同的特点,选择合适的垃圾回收算法,可以帮助优化程序的性能和响应速度。接下来,我们来了解几种常见的垃圾回收算法。

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

标记-清除算法是最简单的一种算法,它分为两个阶段:

  • 标记阶段:标记所有可达对象,也就是程序中被引用的对象。
  • 清除阶段:删除所有没有被标记的对象,释放内存。

标记-清除算法的优点是实现简单,缺点是回收后会产生内存碎片。因为对象被清除后,它们的内存空间没有被整理,导致堆内存中可能会有许多不连续的空闲空间,降低内存使用效率。

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

标记-整理算法与标记-清除算法相似,主要的区别在于清除阶段不同。标记-整理算法不仅会标记并清除垃圾对象,还会将存活的对象压缩到堆的一端,避免内存碎片的产生。

它的缺点是会引入额外的 整理 操作,需要更多的 CPU 时间,但可以更有效地管理内存,避免碎片问题。

3. 复制算法(Copying)

复制算法将堆内存分为两个区域:一个为“当前使用区”,另一个为“空闲区”。每次垃圾回收时,存活的对象会从“当前使用区”复制到“空闲区”。回收完成后,“当前使用区”被完全清空,整个内存交换区域的角色,继续进行下次的垃圾回收。

复制算法的优点是回收速度快,但需要 双倍的内存空间 来存储存活的对象,因此在内存受限的情况下可能不适用。

4. 分代收集算法(Generational Collection)

分代收集算法是目前大多数 JVM 实现采用的策略。它将堆内存分为 年轻代(Young Generation)和 老年代(Old Generation)两个区域。年轻代中包含了 Eden 区 和两个 Survivor 区,新创建的对象首先会在年轻代中分配内存。

  • 年轻代:包含新创建的对象,其中大多数对象会很快成为垃圾。
  • 老年代:存活时间较长的对象会被移动到老年代。

通过将对象根据年龄分代,JVM 可以采用不同的垃圾回收策略来提高效率:年轻代使用 复制算法,老年代使用 标记-清除算法标记-整理算法。这种策略的最大优势是优化了大部分短生命周期对象的回收效率。

4. 常见的垃圾回收器 🧑‍🔧

JVM 提供了多种垃圾回收器来支持不同的回收需求。每种回收器都有不同的特点,适用于不同的场景。

1. Serial 垃圾回收器

Serial 垃圾回收器是最基本的垃圾回收器,它使用单线程执行垃圾回收。适用于单核 CPU 或内存较小的系统。它的优点是实现简单,但缺点是 停顿时间较长,因为回收过程会暂停所有应用线程。

2. Parallel 垃圾回收器

Parallel 垃圾回收器(也称为吞吐量优先垃圾回收器)使用多个线程来进行垃圾回收,适用于多核 CPU 环境,能够大幅提升垃圾回收的效率。它适用于 吞吐量要求较高 的应用,但缺点是停顿时间相对较长,适用于可以容忍一定停顿的场景。

3. CMS(Concurrent Mark-Sweep)垃圾回收器

CMS 是为了减少停顿时间而设计的垃圾回收器,它采用并发回收的方式,在垃圾回收过程中尽量不阻塞应用线程。适用于需要低延迟的场景。虽然 CMS 能够减少停顿时间,但也带来了一些内存碎片的问题,且在多核机器上性能可能不稳定。

4. G1(Garbage First)垃圾回收器

G1 是一个新型垃圾回收器,旨在兼顾低停顿和高吞吐量。G1 将堆内存分为多个小区域(Region),并通过分阶段回收来最大化垃圾回收效率。G1 比 CMS 更加灵活且可控,适用于 大内存、低延迟的系统

5. 如何优化垃圾回收? 🚀

虽然 JVM 自动执行垃圾回收,但开发者仍然可以通过一些策略来优化 GC 的性能。比如:

  • 调整堆大小:合理设置年轻代和老年代的内存大小,避免频繁进行垃圾回收。
  • 选择合适的垃圾回收器:根据应用场景选择合适的回收器。如果需要低延迟,选择 CMS 或 G1;如果吞吐量更重要,可以选择 Parallel。
  • 优化对象创建与销毁:减少对象的创建和销毁频率,避免不必要的垃圾回收。

当然!为了帮助你更好地理解垃圾回收的机制,我们来通过一些具体的代码示例,展示垃圾回收的工作原理和垃圾回收器的使用。

6. 示例演示 🚀

1. GC基础示例:显示垃圾回收信息 🧹

在这个示例中,我们将启用 JVM 的垃圾回收日志,通过代码来触发垃圾回收,并观察输出的垃圾回收信息。

示例代码:

public class GCDemo {

    public static void main(String[] args) {
        // 启用垃圾回收日志
        System.out.println("JVM将会在终端输出垃圾回收日志");
        System.gc();  // 显式调用垃圾回收

        // 创建一些对象来触发垃圾回收
        for (int i = 0; i < 100000; i++) {
            new Person("Person " + i);
        }

        // 手动触发垃圾回收
        System.gc();
    }
}

class Person {
    String name;

    Person(String name) {
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("对象 " + name + " 被回收了!");
    }
}

解释:

  1. System.gc() 是一个显式的垃圾回收调用,它建议 JVM 执行垃圾回收。
  2. 我们创建了大量 Person 对象,然后手动触发垃圾回收。每个 Person 对象在不再被引用时都会被回收,调用 finalize() 方法,输出“对象被回收”的信息。
  3. finalize() 方法是 JVM 回收对象时调用的一个钩子方法,可以用来做一些清理工作。这个方法并不总是被及时调用,但它会在垃圾回收器回收对象前被执行。

运行输出示例:

JVM将会在终端输出垃圾回收日志
对象 Person 12345 被回收了!
对象 Person 12346 被回收了!
...

2. 设置垃圾回收器:使用 G1 垃圾回收器 🧑‍🔧

JVM 提供了多个垃圾回收器,例如 G1 回收器、Parallel 回收器等。在运行程序时,你可以通过 JVM 参数来指定使用的垃圾回收器。下面的示例展示了如何在命令行中启用 G1 垃圾回收器。

启动 G1 垃圾回收器:

java -XX:+UseG1GC -Xmx1g -Xms512m GCDemo

参数解释:

  • -XX:+UseG1GC:指定使用 G1 垃圾回收器。
  • -Xmx1g:设置最大堆内存为 1GB。
  • -Xms512m:设置初始堆内存为 512MB。

G1 垃圾回收器的输出:

[GC (Allocation Failure) [PSYoungGen: 1024K->512K(2048K)] 1024K->256K(4096K), 0.0012345 secs]

解释:

  • GC:标记垃圾回收发生了。
  • PSYoungGen:表示年轻代的回收。
  • 1024K->512K:表示年轻代的大小从 1024K 减少到 512K。
  • 0.0012345 secs:回收花费的时间。

通过这种方式,你可以启用不同的垃圾回收器并查看其日志,帮助你了解垃圾回收的过程和效率。

3. 演示堆的分代回收 🏞️

在垃圾回收过程中,JVM 会根据对象的年龄将其划分到不同的内存区域(年轻代、老年代)。下面是一个简单的示例,演示年轻代与老年代之间的对象晋升和回收。

示例代码:

public class GenerationalGC {

    public static void main(String[] args) {
        // 创建大量短生命周期对象
        for (int i = 0; i < 100000; i++) {
            new TempObject();
        }

        // 创建一个长生命周期的对象,进入老年代
        TempObject longLivingObject = new TempObject();
        
        // 触发垃圾回收
        System.gc();
    }
}

class TempObject {
    private static final int SIZE = 10000;
    private byte[] memory = new byte[SIZE];

    TempObject() {
        // 构造器什么都不做
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("对象被回收");
    }
}

代码说明:

  1. TempObject 是我们用来测试的类,它创建了一个包含 SIZE 大小的字节数组 memory,模拟占用内存的情况。
  2. main 方法中,我们创建了 100000 个 TempObject 实例,模拟短生命周期对象的创建。
  3. 然后创建了一个 longLivingObject,这个对象会存活较长时间,应该被晋升到老年代。
  4. System.gc() 用于显式地触发垃圾回收。

输出示例:

对象被回收
对象被回收
...

解释:

  • 大量短生命周期的对象会被回收,垃圾回收器将它们从年轻代清理掉。
  • 长生命周期的对象则会从年轻代晋升到老年代,最终在老年代被回收。

4. 自定义垃圾回收行为:finalize() 🧹

finalize() 方法是 Java 中的一个钩子方法,当垃圾回收器准备回收对象时,finalize() 方法会被调用。在实际应用中,不建议依赖 finalize() 方法来进行资源清理,因为垃圾回收并不是及时发生的,可能会造成延迟。

示例代码:

public class FinalizeDemo {

    public static void main(String[] args) {
        // 创建一个对象并让它离开作用域
        FinalizeDemo fd = new FinalizeDemo();
        fd = null;

        // 手动触发垃圾回收
        System.gc();
        
        // 给垃圾回收器一些时间来执行 finalize()
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Finalize 方法被调用,垃圾回收器将回收该对象!");
    }
}

解释:

  • 我们通过将 fd 设置为 null 来让对象变得不可达。
  • 然后通过 System.gc() 显式触发垃圾回收,垃圾回收器会在回收该对象前调用 finalize() 方法。

输出示例:

Finalize 方法被调用,垃圾回收器将回收该对象!

7. 总结 📚

垃圾回收是 Java 生态中非常重要的机制,它帮助我们自动管理内存,减少了开发者的负担,但同时也带来了一些性能上的挑战。理解垃圾回收的工作原理,以及如何选择和优化垃圾回收策略,是每个 Java 开发者的必修课。希望这篇文章能够帮助你更好地理解和掌握垃圾回收相关的知识!掌握了 GC,你就能更好地应对 Java 面试中的 GC 问题,提升你的编程技能和面试竞争力!加油哦,GC 的世界比你想象的更精彩!🎉

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。

✨️ Who am I?

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云2023年度十佳博主,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。