强引用和弱引用

无论是File还是Socket等重量级的资源(严重依赖操作系统资源),在进行释放时并不能百分之百的保证成功(可能是操作系统的原因),
对socket的关闭有可能会失败,然后socket的实例会被垃圾回收器回收,但是socket实例对应的底层系统资源或许并未释放。

那么我们有什么办法可以再次尝试对socket进行资源释放操作呢?

1 Strong Reference及LRUCache

1.1 LRUCache

Cache是一种用于提高系统性能,提高数据检索效率的机制,而LRU(Least recently used,最近最少使用)算法和Cache的结合是最常见的一种Cache实现。

关于LRUCache的介绍,可以参考:LRU算法

简单实现一个LRUCache:

public class LRUCache<K, V> {

    /**
     * 用于记录Key值的顺序
     */
    private final LinkedList<K> keyList = new LinkedList<>();

    private final Map<K, V> cache = new HashMap<>();

    /**
     * cache的最大容量
     */
    private final int capacity;

    /**
     * 提供一个加载数据的方式
     */
    private final CacheLoader<K, V> cacheLoader;

    public LRUCache(int capacity, CacheLoader<K, V> cacheLoader) {
        this.capacity = capacity;
        this.cacheLoader = cacheLoader;
    }

    public void put(K k, V v) {
        if (keyList.size() >= capacity) {
            // 当元素数量超过容量时,将最老的数据清除
            // 最先加入缓存的,就是最老的数据
            K k1 = keyList.removeFirst();
            cache.remove(k);
        }
        if (keyList.contains(k)) {
            // 如果数据已经存在,则从key的队列中删除,再将这个key放到队尾
            keyList.remove(k);
        }
        keyList.addLast(k);
        cache.put(k, v);
    }

    public V get(K k) {
        V value;
        // 先将key从key List中删除
        boolean success = keyList.remove(k);
        if (!success) {
            // 如果删除失败,则表明该数据不存在
            // 通过cacheLoader对数据进行加载
            value = cacheLoader.load(k);
            // 加到缓存中
            this.put(k, value);
        } else {
            // 删除成功,则可以从cache中获取数据,再将key放置到队尾
            value = cache.get(k);
            keyList.addLast(k);
        }
        return value;
    }

    @Override
    public String toString() {
        return this.keyList.toString();
    }
}

CacheLoader比较简单,是一种标准的函数式接口:

@FunctionalInterface
public interface CacheLoader<K, V> {
    V load(K k);
}

测试:

 public static void main(String[] args) {
     LRUCache<String,Object> cache = new LRUCache<>(5, k->new Object());
     cache.get("kobe");
     cache.get("james");
     cache.get("wade");
     cache.get("t-mac");
     cache.get("harden");
     System.out.println(cache);
     // 此时就会就把最先添加的kobe剔除
     cache.get("curry");
     System.out.println(cache);
 }
[kobe, james, wade, t-mac, harden]
[james, wade, t-mac, harden, curry]

1.2 Strong Reference

强引用(Strong Reference)是我们平时使用最多的一种对象引用,当一个对象被关键字new实例化出来的时候,JVM会在堆(heap)内存中开辟一片内存区域,用于存放与该实例对应的数据结构。JVM垃圾回收器线程会在达到GC条件的时候尝试回收(Full GC,Young GC)堆栈内存中的数据,强引用的特点是只要引用到ROOT根的路径可达,无论怎样的GC都不会将其释放,而是宁可出现JVM内存溢出。

1.3 内存溢出:

  1. 定义一个Reference,每个Reference对象大约占1M内存
public class Reference {
    /**
     * 1M的数据
     */
    private final byte[] data = new byte[2<<19];

   @Override
   protected void finalize(){
       System.out.println("this reference will be gc");
   }
}
  1. 不断的往Cache中存放数据或者存放固定数量大小(capacity)的数据时,由于是Strong Reference的缘故,可能会引起内存溢出的
 public static void main(String[] args) throws InterruptedException {
     LRUCache<Integer,Reference> cache = new LRUCache<>(5, k->new Reference());
     for (int i = 0; i < Integer.MAX_VALUE; i++) {
         cache.get(i);
         TimeUnit.SECONDS.sleep(1);
         System.out.println("The " + i + " reference stored at cache.");
     }
 }

运行之前先设置一下jvm参数:

-Xmx128M -Xms64M -XX:+PrintGCDetails
  • -Xmx128M:指定最大的堆内存大小。
  • -Xms64M:指定初始化的堆内存大小。
  • -XX:+PrintGCDetails:在控制台输出GC的详细信息。

运行程序大约在插入了98个Reference左右的时候,JVM出现了堆内存溢出,输出如下:

The 98 reference stored at cache.
[Full GC (Ergonomics) [PSYoungGen: 15678K->15362K(18944K)] [ParOldGen: 87104K->87103K(87552K)] 102782K->102465K(106496K), [Metaspace: 4779K->4779K(1056768K)], 0.0147403 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 15362K->15362K(18944K)] [ParOldGen: 87103K->87103K(87552K)] 102465K->102465K(106496K), [Metaspace: 4779K->4779K(1056768K)], 0.0104259 secs] [Times: user=0.13 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 18944K, used 16002K [0x00000000fd580000, 0x00000000fea80000, 0x0000000100000000)
  eden space 16384K, 97% used [0x00000000fd580000,0x00000000fe5209c8,0x00000000fe580000)
  from space 2560K, 0% used [0x00000000fe580000,0x00000000fe580000,0x00000000fe800000)
  to   space 2560K, 0% used [0x00000000fe800000,0x00000000fe800000,0x00000000fea80000)
 ParOldGen       total 87552K, used 87103K [0x00000000f8000000, 0x00000000fd580000, 0x00000000fd580000)
  object space 87552K, 99% used [0x00000000f8000000,0x00000000fd50fd90,0x00000000fd580000)
 Metaspace       used 4810K, capacity 4978K, committed 5248K, reserved 1056768K
  class space    used 530K, capacity 591K, committed 640K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

98个Reference大约占用了100MB左右的堆内存大小,JVM自身启动时也需要加载和初始化很多对象实例。

既然数据是被Cache的,那么能不能在JVM进行垃圾回收的时候帮我们进行数据清除呢?当需要的时候再次加载就可以了,这就是我们接下来需要介绍的内容Soft Reference(软引用)。

2 Soft Reference(软引用)及SoftLRUCache

2.1 SoftLRUCache

使用java.lang.ref.SoftReference包装缓存中的value:

public class SoftLRUCache<K, V> {

    /**
     * 用于记录Key值的顺序
     */
    private final LinkedList<K> keyList = new LinkedList<>();

    /**
     * Value采用SoftReference进行修饰
     */
    private final Map<K, SoftReference<V>> cache = new HashMap<>();

    private final int capacity;
    private final CacheLoader<K, V> cacheLoader;

    public SoftLRUCache(int capacity, CacheLoader<K, V> cacheLoader) {
        this.capacity = capacity;
        this.cacheLoader = cacheLoader;
    }

    public void put(K k, V v) {
        if (keyList.size() >= this.capacity) {
            // 当数据已经到了容量的时候,需要移除最老的元素
            K eldestKey = keyList.removeFirst();
            cache.remove(eldestKey);
        }
        if (keyList.contains(k)) {
            // 如果已经包含了也需要先删除
            keyList.remove(k);
        }
        // 添加到尾部
        keyList.addLast(k);
        cache.put(k, new SoftReference<>(v));
    }

    public V get(K k) {
        V value;
        // 删除
        boolean success = this.keyList.remove(k);
        if (!success) {
            // 如果删除失败,则表明该数据不存在
            // 通过cacheLoader对数据进行加载
            value = this.cacheLoader.load(k);
            // 加到缓存中
            this.put(k, value);
        } else {
            // 删除成功,则可以从cache中获取数据,再将key放置到队尾
            value = this.cache.get(k).get();
            this.keyList.addLast(k);
        }
        return value;
    }

    @Override
    public String toString() {
        return this.keyList.toString();
    }
}

2.2 再次测试内存溢出

使用SoftLRUCache:


public static void main(String[] args) throws InterruptedException {
   SoftLRUCache<Integer,Reference> cache = new SoftLRUCache<>(5, k->new Reference());
   for (int i = 0; i < Integer.MAX_VALUE; i++) {
       cache.get(i);
       TimeUnit.SECONDS.sleep(1);
       System.out.println("The " + i + " reference stored at cache.");
   }
}

程序不论运行多久都不会出现JVM溢出的问题

(但是不代表SoftReference引用不会引起内存溢出,如果cache中插入的速度太快,那么GC线程没有来得及回收对象,很有可能也会引起溢出)

2.3 Soft Reference(软引用)

当JVM Detect(探测)到内存即将溢出,它会尝试GC soft类型的Soft Reference,而不会想强引用只要引用到ROOT根的路径可达,无论怎样的GC都不会将其释放,而是宁可出现JVM内存溢出。

3 Weak Reference (弱引用)

无论是young GC还是full GC Weak Reference的引用都会被垃圾回收器回收,Weak Reference(弱引用)可以用来做Cache,但是一般很少使用。

  1. 任何类型的GC都可导致Weak Reference对象被回收
 public static void main(String[] args) {
     Reference ref = new Reference();
     WeakReference<Reference> weakReference = new WeakReference<Reference>(ref);
     ref = null;
     System.gc();
 }

在上面的代码中我们定义了一个WeakReference,并且显式地将ref设置为null,当调用gc方法时Weak Reference对象将被立即回收,输出如下:

[GC (System.gc()) [PSYoungGen: 5206K->1800K(75776K)] 5206K->1808K(249344K), 0.0011190 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1800K->0K(75776K)] [ParOldGen: 8K->1630K(173568K)] 1808K->1630K(249344K), [Metaspace: 3298K->3298K(1056768K)], 0.0034624 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
this reference will be gc

3.1 ReferenceQueue

无论是SoftReference还是WeakReference引用,被垃圾回收器回收后,都会被存放到与之关联的ReferenceQueue中

public static void main(String[] args) throws InterruptedException {
    ReferenceQueue<Reference> referenceQueue = new ReferenceQueue<>();
    Reference ref = new Reference();
    // 传入referenceQueue
    WeakReference<Reference> weakReference = new WeakReference<Reference>(ref,referenceQueue);
    ref = null;
    System.gc();
    // 休眠一下,确保gc线程执行
    TimeUnit.SECONDS.sleep(1);

    // remove方法是阻塞方法
    java.lang.ref.Reference<? extends Reference> remove = referenceQueue.remove();

    // 被垃圾回收之后,会从队列中获得
    System.out.println("===================================");
    System.out.println(remove);
    System.out.println("===================================");
}

4 Phantom Reference(虚引用)

Phantom reference objects,which are enqueued after the collector determines that their referents may otherwise be reclaimed.Phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism.

这是JDK官网对Phantom reference(幻影引用)的说明,与SoftReference和WeakRe-fe-rence相比较Phantom Reference有如下几点不同之处:

  • Phantom Reference必须和ReferenceQueue配合使用。
  • Phantom Reference的get方法返回的始终是null。
  • 当垃圾回收器决定回收Phantom Reference对象的时候会将其插入关联的ReferenceQueue中。
  • 使用Phantom Reference进行清理动作要比Object的finalize方法更加灵活。

4.1 Phantom Reference使用:资源释放

多线程设计模式:Two Phase Termination设计模式
这一章会去释放socket资源,为了提高释放socket资源的概率,就可以使用Phantom Reference改进:

  1. SocketCleaningTracker
class SocketCleaningTracker {
	// 定义ReferenceQueue
    private static final ReferenceQueue<Object> queue = new ReferenceQueue<>();

    static {
    	// 启动Cleaner线程
        new Cleaner().start();
    }

    private static void track(Socket socket) {
        new Tracker(socket, queue);
    }

    private static class Cleaner extends Thread {
        private Cleaner() {
            super("SocketCleaningTracker");
            // 设置为守护线程
            setDaemon(true);

        }

        @Override
        public void run() {
            for (; ; ) {
                try {
                	// 当Tracker被垃圾回收器回收时会加入Queue中
                    Tracker tracker = (Tracker) queue.remove();
                    // 在执行一次close
                    tracker.close();
                } catch (InterruptedException e) {
                }
            }
        }
    }
	// Tracker是一个PhantomReference的子类:包装立刻
    private static final class Tracker extends PhantomReference<Object> {
        private final Socket socket;

        Tracker(Socket socket, ReferenceQueue<? super Object> queue) {
            super(socket, queue);
            this.socket = socket;
        }

        public void close() {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 修改release方法
private void release() {
    try {
        if (this.socket != null) {
            socket.close();
        }
    } catch (Throwable e) {
       if (socket!= null){
       		// 将socket实例加入Tracker中 
           new SocketCleaningTracker.tracker(socket);
      }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值