MST

星途 面试题库

面试题:深入分析Java垃圾回收机制与内存泄漏避免的关系

深入探讨Java垃圾回收机制的工作原理,说明如何利用对垃圾回收机制的理解来有效避免内存泄漏,包括在复杂的多线程环境下可能遇到的内存泄漏问题及解决思路。
26.0万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

Java垃圾回收机制工作原理

  1. 可达性分析算法: Java 虚拟机通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”。如果某个对象到 GC Roots 间没有任何引用链相连,说明此对象不可达,即成为可回收对象。GC Roots 通常包括虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象等。
  2. 垃圾回收算法
    • 标记 - 清除算法:先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法效率问题在于标记和清除过程的效率都不高,且会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
    • 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法实现简单,运行高效,但代价是将内存缩小为原来的一半。
    • 标记 - 整理算法:标记过程仍然与“标记 - 清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
    • 分代收集算法:根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量对象存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记 - 清除”或“标记 - 整理”算法来进行回收。

利用垃圾回收机制理解避免内存泄漏

  1. 正确使用引用
    • 强引用:若一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。所以在不使用对象时,应及时将强引用置为 null,比如:Object obj = new Object(); // 创建对象,obj 为强引用,使用完后 obj = null;,这样垃圾回收器就可以对该对象进行回收。
    • 软引用:用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。可以使用 SoftReference 类来创建软引用,如 SoftReference<String> softRef = new SoftReference<>(new String("example"));,适用于缓存等场景,当内存不足时可被回收。
    • 弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。通过 WeakReference 类创建,例如 WeakReference<String> weakRef = new WeakReference<>(new String("example"));
    • 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。使用 PhantomReference 类创建。
  2. 集合类的使用
    • 当向集合(如 ArrayListHashMap 等)中添加对象后,如果后续不再使用这些对象,应从集合中移除。否则,即使这些对象在其他地方不再被引用,但由于集合对它们的引用,垃圾回收器无法回收这些对象,从而导致内存泄漏。例如在一个 ArrayList 中添加元素:ArrayList<Integer> list = new ArrayList<>(); list.add(1);,如果后续不再需要这个元素,应使用 list.remove(Integer.valueOf(1)); 移除。
    • 对于 HashMap,如果以对象作为键,当键对象的状态发生改变可能影响其 hashCode 值时,可能导致在 HashMap 中无法正确定位该键值对,从而无法从 HashMap 中移除该键值对,造成内存泄漏。所以作为 HashMap 键的对象,其影响 hashCode 的状态不应改变。

多线程环境下的内存泄漏问题及解决思路

  1. 线程局部变量导致的内存泄漏
    • 问题:在多线程环境中,如果线程局部变量(ThreadLocal)使用不当,可能会导致内存泄漏。ThreadLocal 为每个使用该变量的线程提供独立的变量副本,若在线程结束时没有正确清理 ThreadLocal 中的数据,即使线程结束,ThreadLocal 所引用的对象可能因为线程持有而无法被垃圾回收器回收。
    • 解决思路:在 ThreadLocal 使用完毕后,调用 ThreadLocalremove() 方法清除线程局部变量中的数据。例如:
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void someMethod() {
    threadLocal.set("example");
    // 使用 threadLocal 中的数据
    // 使用完毕后
    threadLocal.remove();
}
  1. 线程池导致的内存泄漏
    • 问题:在使用线程池时,如果任务提交到线程池后,任务内部持有了外部对象的引用,而任务执行时间过长或线程池中的线程生命周期长,外部对象可能因为被任务引用而无法被垃圾回收,即使外部对象不再被其他地方使用。
    • 解决思路:尽量避免在任务内部持有外部对象的强引用。如果确实需要,可以使用弱引用或软引用来持有外部对象。例如,在任务类中定义一个弱引用成员变量:
class MyTask implements Runnable {
    private WeakReference<ExternalObject> weakRef;
    public MyTask(ExternalObject obj) {
        this.weakRef = new WeakReference<>(obj);
    }
    @Override
    public void run() {
        ExternalObject obj = weakRef.get();
        if (obj != null) {
            // 执行任务逻辑
        }
    }
}
  1. 监听器和回调导致的内存泄漏
    • 问题:在多线程环境中,当注册监听器或回调时,如果没有及时取消注册,被监听或回调的对象会持有监听器或回调对象的引用,导致监听器或回调对象无法被垃圾回收,即使它们不再被需要。
    • 解决思路:在适当的时候,如对象销毁前,取消监听器的注册或回调的设置。例如,在一个图形界面应用中,如果注册了按钮点击监听器:
Button button = new Button();
MyClickListener listener = new MyClickListener();
button.addActionListener(listener);
// 在窗口关闭等适当时候
button.removeActionListener(listener);