1. 局部变量存放在哪里
先从最简单的开始:局部变量。它们是在方法内部声明,只在方法执行期间存在的变量。它们的生命短暂而戏剧性:一旦方法结束,所有局部变量都会悄然消失。
在 Java 中,局部变量存放在栈上。每个线程都有自己的栈。如果一个方法调用了另一个方法,栈中会添加一个新的“帧”(stack frame),其中包含局部变量和返回地址。当方法完成时,它的帧会从栈中移除。
示例:局部变量的一生
public class LocalVariableDemo {
public static void main(String[] args) {
int a = 42; // 局部变量 a 只在 main 中存活
printSquare(a);
// 这里变量 b 已不存在!
}
public static void printSquare(int b) {
int square = b * b; // 局部变量 square
System.out.println("平方:" + square);
// 退出 printSquare 后,所有局部变量都会消失
}
}
重点:如果局部变量是指向对象的引用(例如 String、Scanner、数组),那么引用本身位于栈上,而对象位于堆上!当引用消失且没有其他引用指向该对象时,垃圾收集器可以删除该对象。
示意
Stack (针对 main):
| int a = 42 |
| args |
-------------------
Heap:
| [通过 new 创建的对象] |
2. Java 中的内存泄漏:神话还是现实?
很多新手以为:“Java 有垃圾收集器!那就不会有内存泄漏!”可惜,这个神话在第一个大型项目上就会破灭。
垃圾收集器只会删除没有任何存活引用指向的对象。如果某处还残留着一个引用(哪怕在最意想不到的地方),对象就会一直挂在内存里直到天荒地老,或者直到抛出 OutOfMemoryError。
示例 1:静态集合陷阱
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
// 哦,这个静态集合!
private static final List<String> BIG_LIST = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
BIG_LIST.add("第 " + i + " 条字符串");
}
System.out.println("已添加一百万条字符串");
// 即使 main 结束,只要 JVM 存活,BIG_LIST 仍会留在内存中
}
}
发生了什么?
- 静态变量 BIG_LIST 的生命周期与类一致(通常到 JVM 结束为止)。
- 添加到列表中的所有字符串都不会被 GC 删除——始终通过 BIG_LIST 有引用指向它们。
- 如果你忘记清理这样的集合——就会产生内存泄漏。
示例 2:未取消订阅的监听器(listeners)
import java.util.ArrayList;
import java.util.List;
class EventSource {
private final List<Runnable> listeners = new ArrayList<>();
public void addListener(Runnable listener) {
listeners.add(listener);
}
// ... 其他方法 ...
}
public class ListenerLeakDemo {
public static void main(String[] args) {
EventSource source = new EventSource();
Runnable listener = () -> System.out.println("事件!");
source.addListener(listener);
// 如果忘记调用 source.removeListener(listener),listener 将会一直留在内存中!
}
}
问题:如果监听器不再需要,但没有从列表中移除——它及其所引用的所有对象都会留在内存中。
示例 3:永不清理的缓存
import java.util.HashMap;
import java.util.Map;
public class CacheLeakDemo {
private static final Map<String, byte[]> CACHE = new HashMap<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
// 每次创建 1 KB 的数组
CACHE.put("key" + i, new byte[1024]);
}
System.out.println("已向缓存添加了一百万个元素");
// 缓存持续增长,内存耗尽,OutOfMemoryError!
}
}
结论:即使有 GC,如果不关注对象的生命周期,也很容易产生内存泄漏!
3. 弱引用(WeakReference)及其朋友们
有时我们需要这样的缓存或集合:当对象不再被其他地方引用时,允许 GC 把它们回收。为此就有了弱引用(WeakReference)。
普通(strong)引用
String s = new String("hello"); // strong 引用
对象 s 只要还有至少一个 strong 引用,就会一直活在内存中。
弱引用(WeakReference)
import java.lang.ref.WeakReference;
public class WeakRefDemo {
public static void main(String[] args) {
String strong = new String("你好,世界!");
WeakReference<String> weak = new WeakReference<>(strong);
System.out.println("清理前:" + weak.get()); // 仍有引用
strong = null; // 移除 strong 引用
System.gc(); // 请求 GC 清理内存(不保证!)
// 一段时间后,weak.get() 可能变为 null
System.out.println("GC 之后:" + weak.get());
}
}
它如何工作?
- 只要还有至少一个 strong 引用,GC 就不会删除对象。
- 如果只剩下弱引用,对象可能会在下一次 GC 时被删除。
- 方法 weak.get() 会在对象仍存活时返回对象,否则返回 null。
弱引用的应用场景?
主要用于缓存:如果对象从内存中被删除也无伤大雅。例如缓存图片,但又不希望缓存占满所有内存。
示例:WeakHashMap
WeakHashMap 是一种键通过弱引用保存的集合。如果没有其他地方再引用该键,对应的键值对会从映射中移除。
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Object, String> map = new WeakHashMap<>();
Object key = new Object();
map.put(key, "值");
System.out.println("清理前:" + map);
key = null; // 移除对键的 strong 引用
System.gc(); // 请求 GC 工作
// 一段时间后,map 可能变为空!
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("GC 之后:" + map);
}
}
注意:WeakHashMap 只对键使用弱引用——值仍通过普通 strong 引用保存。
Soft、Weak、Phantom:引用家族全览
Java 中有四种引用(按“强度”排序):
| 引用类型 | 对象何时被 GC 删除? | 适用场景 |
|---|---|---|
|
仅当不存在任何 strong 引用时 | 普通变量、集合 |
|
内存不足时 | 希望尽量长期保留的缓存 |
|
若不存在 strong 引用,则在下一次 GC 时 | 缓存、WeakHashMap、监听器 |
|
在终结之后,用于跟踪对象被删除 | 特殊任务、堆外清理 |
- SoftReference——内存不足时对象会被删除(适合图像等缓存)。
- WeakReference——只要没有其他引用,会在第一次 GC 时删除。
- PhantomReference——最“幽灵”的类型,用于复杂场景,新手很少使用。
4. 实战:一个内存泄漏示例及其修复
泄漏示例:静态列表
import java.util.ArrayList;
import java.util.List;
public class LeakExample {
private static final List<byte[]> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new byte[1024 * 1024]); // 1 MB
if (i % 10 == 0) System.out.println("已添加 " + i + " MB");
}
}
}
会发生什么?
程序会很快吃光所有可用内存,并因 OutOfMemoryError 崩溃,因为静态列表保存着对所有已创建数组的引用。
修复:使用弱引用
如果不要求所有对象始终可用,可以通过弱引用来保存它们:
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class LeakFixed {
private static final List<WeakReference<byte[]>> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new WeakReference<>(new byte[1024 * 1024]));
if (i % 10 == 0) System.out.println("已添加 " + i + " MB");
System.gc(); // 给 GC 的提示(不保证立即清理!)
}
}
}
现在,如果没有其他地方再引用这些数组,GC 就可以删除它们——列表只保存弱引用。
5. 典型的内存泄漏场景
- 事件监听器:忘了移除监听器——对象就会“长生不老”。
- 静态集合:不清理的缓存、全局列表——都可能导致泄漏。
- 内部类与 lambda:如果内部类或 lambda 捕获了外部对象的引用,只要外部对象存活,它就不会被删除。
内部类示例
public class Outer {
private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 MB
public Runnable createTask() {
// 匿名内部类会捕获对 Outer 的引用!
return new Runnable() {
@Override
public void run() {
System.out.println("任务正在执行");
}
};
}
public static void main(String[] args) {
Outer outer = new Outer();
Runnable task = outer.createTask();
// 即使 outer = null,task 仍然持有对 bigArray 的引用!
}
}
解决方案:使用静态内部类,或将逻辑拆分到独立类,以避免持有不必要的引用。
6. 实战:在缓存中使用弱引用
在教学应用中添加一个使用 WeakHashMap 的简易缓存。
import java.util.Map;
import java.util.WeakHashMap;
public class ImageCache {
private final Map<String, byte[]> cache = new WeakHashMap<>();
public void put(String name, byte[] data) {
cache.put(name, data);
}
public byte[] get(String name) {
return cache.get(name);
}
public static void main(String[] args) {
ImageCache cache = new ImageCache();
cache.put("cat", new byte[1024 * 1024]); // 1 MB
System.out.println("小猫已加入缓存");
// 如果对键“cat”不再有任何引用,条目可能被 GC 删除
}
}
在真实应用中(例如图像库),弱引用通过自动删除不常用数据,有助于避免内存耗尽。
7. Strong vs Weak:该用哪种?
- Strong 引用——默认选择:用于必须保证存活的对象。
- 弱引用——用于缓存、监听器,当对象被删除也无关紧要时。
- Soft 引用——用于希望尽量保留、但内存不足时可删除的缓存。
- Phantom 引用——高级场景(例如堆外的清理)。
8. 使用内存与引用时的常见错误
错误 1:“GC 会替我处理一切!” 垃圾收集器只会删除没有任何存活 strong 引用的对象。如果你在某处“遗留”了引用(例如在 static 集合中),对象会一直存活。
错误 2:遗忘的监听器。给对象添加了监听器,却没有在销毁时移除它?监听器及其捕获的一切都会留在内存中。
错误 3:没有弱引用的缓存。用普通的 HashMap 实现需要自动清理的缓存?最好使用 WeakHashMap 或 SoftReference。
错误 4:内部类与 lambda 捕获外部对象。内部类(以及 lambda 表达式)会隐式保存对外部对象的引用。如果这些实例的生命周期长于外部对象,就会发生泄漏。
错误 5:期望 GC 立即生效。调用 System.gc() 并不保证马上进行垃圾回收。这只是对 JVM 的“请求”,而非命令。
GO TO FULL VERSION