防止内存泄露之让 Map 的 Key 没有其他强引用时可以被回收

2023-03-24 08:37:01 浏览数 (1)

一、问题描述

在开发中遇到一个困境,需要在某个类(如 ValueHolder)中设计一个 Map 其中 Key 是另外一个类型 (如Source)。Source有自己的生命周期,由于ValueHolder 的生命周期较长,在 Source生命周期结束后就应该回收,但由于被 ValueHolder 所持有导致无法被回收,从而导致内存泄露。

二、背景知识

2.1 对象存活的判断

判断 Java 对象是否可以被回收,有两种常见的方法:

  • 引用计数法:给对象中添加一个引用计数器,每当有一个引用指向它时,计数器值加1;每当有一个引用失效时,计数器值减1。任何时刻计数器值为0的对象就是不可能再被利用的,那么这个对象就是可回收对象。
  • 可达性分析,是一种判断 Java 对象是否可以被回收的方法。它的基本思想是,从一系列称为GC Roots的对象作为起点,沿着引用链向下搜索,如果某个对象没有任何路径与GC Roots相连,则说明该对象是不可达的,那么这个对象就是可回收对象。 GC Roots 通常包括以下几种类型的对象:
    • 虚拟机栈中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象

可达性分析可以有效地解决循环引用的问题,即便两个或多个对象相互引用,只要从GC Roots出发无法到达它们,那么它们就都是可回收对象。 Java虚拟机并不使用引用计数法来判断对象是否可以被回收,因为这种方法无法解决循环引用的问题。Java虚拟机主要使用可达性分析法来进行垃圾回收。

2.2 引用的类型

  • 强引用(Strong Reference)是最常见的普通对象引用,只要还有强引用指向一个对象,对象就存活,垃圾回收器不会处理存活对象。强引用可能会导致内存泄漏。
  • 软引用(Soft Reference)是一种相对弱化了一些的引用,可以让对象豁免一些垃圾收集。当系统内存充足时,不会被回收;当系统内存不足时,会被回收。软引用一般用于对内存敏感的程序中,比如高速缓存。
  • 弱引用(Weak Reference)是一种更弱化了的引用,对于弱引用指向的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否够,都会回收该对象的占用内存。
  • 虚引用(Phantom Reference)是一种形同虚设的引用,它并不能决定对象的生命周期。任何时候这个只有虚引用的对象都有可能被回收。因此,虚引用主要用来跟踪对象的回收状态。

因此我们可以采用弱引用这个知识点来解决这个问题。

三、实现思路

3.1 使用 WeakHashMap

WeakHashMap是一种基于弱引用的动态散列表,它可以实现“自动清理”的内存缓存。当它的键对象没有被其他强引用引用时,垃圾回收器会回收它和对应的值对象,从而避免内存泄漏或浪费。 WeakHashMap的使用场景有以下几种:

  • 缓存系统:使用 WeakHashMap可以作为二级缓存,存放过期或低频数据,当内存不足时,可以自动释放这些数据。
  • 监听器或回调函数:使用 WeakHashMap可以避免因为监听器或回调函数的强引用导致被监听或回调的对象无法被回收。
  • 线程局部变量:使用 WeakHashMap可以作为线程局部变量的容器,当线程结束时,可以自动清理线程局部变量。
  • 其他需要自动清理机制的场景。

本场景就是需要没有其他强引用时,自动回收,避免内存泄露。

但是 WeakHashMap 也存在一些缺点:

  • WeakHashMap 的行为取决于垃圾回收器的运行时机,这是不可预测的。因此,您不能确定 WeakHashMap 中的元素何时被移除。
  • WeakHashMap 不是线程安全,如果多个线程同时访问或修改它,可能会导致不一致或并发异常。需要使用同步机制来保证线程安全。
  • WeakHashMap 的迭代器(Iterator不支持快速失败(fail-fast)机制,也就是说,在迭代过程中如果有其他线程修改了 WeakHashMap,迭代器不会抛出 ConcurrentModificationException 异常。
  • WeakHashMap性能可能不如 HashMap,因为它需要额外的工作来处理弱引用和垃圾回收。

采用这种方案的好处是不需要手动处理 Key 的释放,但是多线程场景下,需要额外做同步。

3.2 使用 WeakReference

WeakReference 是一种弱引用,它可以用来描述非必须存在的对象,当它指向的对象没有被其他强引用引用时,垃圾回收器会回收它。 因此,可以采用 WeakReference 包装 Key ,这样 Source 没有其他强引用时就可以被回收。

当然WeakReference 也存在一些缺点:

  • WeakReference 不能保证对象的存活时间,当对象只被 WeakReference 引用时,它随时可能被垃圾回收器回收,这可能导致一些意外的情况或者数据丢失。
  • WeakReference 需要额外的内存空间和时间来维护引用队列和弱引用对象,这可能影响程序的性能和效率。
  • WeakReference 不能防止内存泄漏,如果弱引用对象本身没有被及时清理或者释放,它仍然会占用内存空间。
  • WeakReference 不能单独使用,它需要配合其他强引用或者软引用来实现缓存或者监听等功能。

采用这种方案,好处是可以和线程安全的 Map 结合,更容易做到线程安全,但需要自己去合适的时机清理。

四、代码实现

代码语言:javascript复制
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Source {

    private String id;
}
代码语言:javascript复制
import lombok.Data;

import java.util.Map;
import java.util.WeakHashMap;

@Data
public class ValueHolder {

    private Map<Source, String> map = new HashMap<>(8);

    public void putValue(Source source, String value) {
        map.put(source, value);
    }

    public void print() {
        System.out.println(map);
    }
}

测试代码:

代码语言:javascript复制
package org.example.demo.weak;

import java.util.concurrent.TimeUnit;

public class WeakHashMapDemo {
    private static final ValueHolder valueHolder = new ValueHolder();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 100; i  ) {
            test("index"   i);
            if (i % 10 == 0) {
                System.gc();
                TimeUnit.MILLISECONDS.sleep(30);
               
                valueHolder.print();
            }
        }
    }

    private static void test(String id) {
        Source source = new Source(id);
        String value = "test";
        valueHolder.putValue(source, value);
    }
}

根据可达性分析可知, valueHolder 在 main 方法执行完毕前都会被 GCRoot (valueHolder) 引用,由于 SourceValueHoder 中的 Map 所持有,在 test 执行完毕后就无法被释放。 那么,本文要实现的效果就是在 test 方法执行完毕后,就允许 Source 被回收。

4.1 使用 WeakHashMap

可以将 HashMap 替换成 WeakHashMap 即可。

代码语言:javascript复制
import lombok.Data;

import java.util.Map;
import java.util.WeakHashMap;

@Data
public class ValueHolder {

    private Map<Source, String> map = new WeakHashMap<>(8);

    public void putValue(Source source, String value) {
        map.put(source, value);
    }

    public void print() {
        System.out.println(map);
    }
}

如果想保证线程安全,可以使用 Collections.synchronizedMap进行包装。

代码语言:javascript复制
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;

@Data
public class ValueHolder {

    private Map<Source, String> map = Collections.synchronizedMap(new WeakHashMap<>(8));

    public void putValue(Source source, String value) {
        synchronized (map) {
            map.put(source, value);
        }
    }

    public void print() {
        synchronized (map) {
            System.out.println(map);
        }
    }
}

测试代码:

代码语言:javascript复制
package org.example.demo.weak;

import java.util.concurrent.TimeUnit;

public class WeakHashMapDemo {
    private static final ValueHolder valueHolder = new ValueHolder();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 100; i  ) {
            test("index"   i);
            if (i % 10 == 0) {
                System.gc();
                TimeUnit.MILLISECONDS.sleep(30);
               
                valueHolder.print();
            }
        }
    }

    private static void test(String id) {
        Source source = new Source(id);
        String value = "test";
        valueHolder.putValue(source, value);
    }
}

4.2 使用 WeakReference

可以使用 WeakReference 对 Key 进行封装,这样 Source 没有其他强引用时会释放。

代码语言:javascript复制
import lombok.Data;

import java.util.Map;
import java.util.WeakHashMap;
import lombok.Data;

import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

@Data
public class ValueHolder {

    private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);

    public void putValue(Source source, String value) {
        map.put(new WeakReference<>(source), value);
    }

    public void print() {
        System.out.println("mapSize:"   map.size());
        for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {
            System.out.println("element:"   entry.getKey().get());
        }
    }
}

存在一个问题, Source 可以被回收,但是 WeakReference 不会被回收。

(1) 可以设计一个 clear 方法,去将已经回收的 Source 对应的数据给清除掉。

代码语言:javascript复制
package org.example.demo.weak;

import lombok.Data;

import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Data
public class ValueHolder {

    private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);

    public void putValue(Source source, String value) {
        map.put(new WeakReference<>(source), value);
    }

    public void print() {
        System.out.println("mapSize:"   map.size());
        clear();
         for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {
            System.out.println("element:"   entry.getKey().get());
        }
    }

    public void clear() {
        Iterator<Map.Entry<WeakReference<Source>, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<WeakReference<Source>, String> entry = iterator.next();
            WeakReference<Source> key = entry.getKey();
            if (key.get() == null) {
                iterator.remove();
            }
        }
    }
}

(2) 还可以在构造 WeakReferece 时传入队列,回收的 Source 会自动放到队列中,定时清理即可。

代码语言:javascript复制
import lombok.Data;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Data
public class ValueHolder {

    private Map<WeakReference<Source>, String> map = new ConcurrentHashMap<>(8);
    private ReferenceQueue<Source> queue = new ReferenceQueue<>();

    public void putValue(Source source, String value) {
        map.put(new WeakReference<>(source, queue), value);
    }

    public void print() {
        System.out.println("mapSize:"   map.size());
        clear();
        for (Map.Entry<WeakReference<Source>, String> entry : map.entrySet()) {
            System.out.println("element:"   entry.getKey().get());
        }
    }

    public void clear() {
        WeakReference<Source> ref;
        while ((ref = (WeakReference<Source>) queue.poll()) != null) {
            map.remove(ref);
        }
    }
}

测试代码:

代码语言:javascript复制
package org.example.demo.weak;

import java.util.concurrent.TimeUnit;

public class WeakHashMapDemo {
    private static final ValueHolder valueHolder = new ValueHolder();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 100; i  ) {
            test("index"   i);
            if (i % 10 == 0) {
                System.gc();
                TimeUnit.MILLISECONDS.sleep(30);
                valueHolder.clear();
                valueHolder.print();
            }
        }
    }

    private static void test(String id) {
        Source source = new Source(id);
        String value = "test";
        valueHolder.putValue(source, value);
    }
}

五、总结

虽然很多人调侃,“面试造轮子,进去拧螺丝”然而当你真正面临复杂问题时,面试中常问的知识点还是非常重要的。扎实的专业基础能够帮助你快速寻找到解决问题的思路。 另外,解决问题的方法不止有一种,需要对比利弊综合分析,选择一个更适合的方案。 此外,现在人工智能的时代已经来临,大家可以尝试使用 AI 来寻找解决问题思路、甚至可以使用 AI 来帮我完成一些基础的代码。

0 人点赞