Java 避坑指南:Scanner 不关闭到底会不会内存泄漏?
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) 这行代码到底创造了什么。它其实跨越了两个维度的资源:
- Java 堆内存(Heap):
scan这个对象本身占用的内存空间。 - 操作系统资源(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)。
“凡打开,必关闭;资源用完,立刻归还。”
将这条铁律刻在肌肉记忆里,是你从新手走向成熟工程师、写出健壮高可用代码的第一步。
更多推荐


所有评论(0)