Java 避坑指南:Scanner 不关闭到底会不会内存泄漏?

在 Java 编程的入门阶段,我们几乎都写过这样一行代码:

在这里插入图片描述

Scanner scan = new Scanner(System.in);
int num = scan.nextInt();

当代码写完,IDE(比如 IDEA)通常会在 scan 下面画一条黄色的波浪线,提示你:“Resource leak: ‘scan’ is never closed” (资源泄漏:scan 从未被关闭)。

很多同学会产生疑问:“Java 不是有神奇的垃圾回收器(GC)吗?对象用完不就自动回收了吗,为什么还要我手动 close()?”

甚至有人会抬杠:“我写算法题从来不关 Scanner,程序也没崩溃过啊!”

今天,我们就来彻底扒开 JVM 的外衣,把 “Java 对象内存” 和 “操作系统底层资源” 这两个极易混淆的概念讲得明明白白。


一、 破局:GC 到底能回收什么?

要回答这个问题,首先要明确 Scanner scan = new Scanner(System.in) 这行代码到底创造了什么。它其实跨越了两个维度的资源:

  1. Java 堆内存(Heap):scan 这个对象本身占用的内存空间。
  2. 操作系统资源(OS Resources):System.in 关联的底层标准输入流,在 Linux/Windows 底层,这被称为**文件描述符(File Descriptor)**或句柄。

核心结论:

  • Java 的 GC(垃圾回收机制)非常尽职,但它的权力是有边界的。 它只管 Java 堆内存里的对象。当 scan 对象不再被使用时,GC 会准时回收它占用的内存。
  • 但是,GC 管不到操作系统级别的资源! 那个被打开的“文件描述符”,如果没有被明确执行 close() 指令,它就会一直保持打开状态。
资源类型举例谁负责回收?是否自动释放?
Java 对象内存new Scanner() 产生的实例JVM 的垃圾回收器 (GC)✅ 是(只要没有引用指向它)
操作系统底层资源文件流、网络 Socket、数据库连接、System.in操作系统 (OS)❌ 否(必须由程序显式调用 close,或等进程彻底死亡)

二、 既然不自动释放,为什么我的小程序没崩溃?

很多同学做力扣(LeetCode)或写期末大作业时,从不关闭 Scanner,程序依然跑得顺风顺水。这又是为什么呢?

因为操作系统(OS)为你“兜了底”。

对于这种**“一次性、短寿命”**的控制台小程序,当 main 方法执行完毕,整个 Java 进程(Process)就结束了。操作系统一旦发现进程死亡,会立刻展现出“冷酷无情”的一面:强制收回该进程申请的所有内存和文件描述符。

所以,在单次运行的小程序中,不关闭 Scanner 确实**“看似”没有影响。但这属于被动兜底**,绝不是正确的编程姿势。


三、 危险边缘:不关闭 Scanner 的真实灾难现场

一旦你的代码脱离了“一次性脚本”的范畴,走向了真实的工业级生产环境(比如持续运行的服务器后台),不关闭底层资源将引发灾难性的后果。

1. 致命打击:文件描述符泄漏 (File Descriptor Leak)

这是后端开发中最臭名昭著的 Bug 之一。

操作系统给每个进程分配的“文件描述符”数量是有限的(例如 Linux 默认通常是 1024 个)。你可以把它想象成超市储物柜的钥匙。

如果你写了一个监控程序,每 10 秒去读取一次终端输入:

// 灾难代码示例
while(true) {
    Scanner scan = new Scanner(System.in);
    String cmd = scan.nextLine();
    // ... 执行逻辑,但不关闭 scan
    Thread.sleep(10000);
}

每次 new Scanner 都会占用一把新钥匙,由于没调用 close(),钥匙一直不归还。几个小时后,1024 把钥匙全部耗尽。

此时,如果你的程序想去读写一个日志文件,或者接收一个前端的 HTTP 网络请求,系统会直接无情地抛出致命异常:

java.io.IOException: Too many open files

程序瞬间瘫痪,且无法自愈,只能重启。

2. 隐蔽的 Bug:流状态异常与阻塞

即便不涉及循环创建,在复杂的逻辑中重用输入流也极易出错。

由于 System.in 是一个全局单例的流,如果不关闭旧的 Scanner 就直接用新的逻辑去读取,未关闭的 Scanner 可能在底层缓冲(Buffer)了部分数据,导致后续读取操作发生错乱:

  • 读不到期望的输入。
  • 抛出 NoSuchElementException。
  • 线程莫名其妙被挂起阻塞。

四、 进阶思考:正确的关闭姿势与避坑

既然必须关,那怎么关?

1. 传统的 try-finally

Scanner scan = null;
try {
    scan = new Scanner(System.in);
    // ... 业务逻辑
} finally {
    if (scan != null) {
        scan.close(); 
    }
}

注意:调用 scan.close() 时,它内部会自动调用底层 System.in.close()。一旦关闭,同一个程序里再也无法从控制台读取输入了(通常这也符合我们的预期,因为输入已经结束)。

2. 现代的 try-with-resources(强烈推荐)

从 Java 7 开始,引入了极为优雅的语法糖。只要类实现了 AutoCloseable 接口(Scanner 实现了该接口),就可以这样写:

// 把资源的创建放在 try 的圆括号里
try (Scanner scan = new Scanner(System.in)) {
    int num = scan.nextInt();
    System.out.println("你输入了:" + num);
} // 大括号结束时,JVM 会自动隐式调用 scan.close()!

这种写法不仅代码清爽,而且彻底杜绝了“忘记写 close”的人为失误。

结语:培养资源嗅觉

回到最初的黄线警告,IDE 并不是在无理取闹,它是在培养你对**“非内存资源”**的敬畏之心。

今天你忽略的是一个不起眼的 Scanner,明天在写真实业务时,你可能就会忘记关闭一个宝贵的 MySQL 数据库连接池(Connection),或者忘记关闭一个占用极大带宽的网络套接字(Socket)。

“凡打开,必关闭;资源用完,立刻归还。”

将这条铁律刻在肌肉记忆里,是你从新手走向成熟工程师、写出健壮高可用代码的第一步。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐