CodeGym /课程 /JAVA 25 SELF /局部变量、内存泄漏、弱引用

局部变量、内存泄漏、弱引用

JAVA 25 SELF
第 64 级 , 课程 2
可用

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 后,所有局部变量都会消失
    }
}

重点:如果局部变量是指向对象的引用(例如 StringScanner、数组),那么引用本身位于栈上,而对象位于堆上!当引用消失且没有其他引用指向该对象时,垃圾收集器可以删除该对象。

示意

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 引用时 普通变量、集合
Soft
内存不足时 希望尽量长期保留的缓存
Weak
若不存在 strong 引用,则在下一次 GC 时 缓存、WeakHashMap、监听器
Phantom
在终结之后,用于跟踪对象被删除 特殊任务、堆外清理
  • 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 实现需要自动清理的缓存?最好使用 WeakHashMapSoftReference

错误 4:内部类与 lambda 捕获外部对象。内部类(以及 lambda 表达式)会隐式保存对外部对象的引用。如果这些实例的生命周期长于外部对象,就会发生泄漏。

错误 5:期望 GC 立即生效。调用 System.gc() 并不保证马上进行垃圾回收。这只是对 JVM 的“请求”,而非命令。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION