转载

JAVA基础(二)内存优化-使用Java引用做缓存

JAVA基础(二)内存优化-使用Java引用做缓存
Java极客  |  作者  /  铿然一叶
这是 Java极客 的第 53 篇原创文章

1. 引用类型

Java中引用类型有以下几类:

类型 描述
强引用 对象具有强引用,不会被垃圾回收,即使发生OutOfMemoryError。
软引用 对象具有软引用,在内存空间足够时,垃圾回收器不会回收它;当内存空间不足时,就会回收这些对象。
弱引用 对象具有弱引用,垃圾回收时如果发现了它就会被回收,而不管内存是否足够。
虚引用 对象具有弱引用,在任何时候都可能被垃圾回收器回收。

根据软引用和弱引用的特点,他们适合做内存敏感应用的缓存,当内存不足时会被回收,同时需要注意的是这些缓存是否高频访问,如果缓存不可用,会不会导致数据库压力过大而挂掉,如果会则还要考虑熔断,降级等处理。

2. 强引用

JAVA中,一般对象的定义都是强引用。显式地设置强引用对象为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象,具体什么时候收集这要取决于gc的算法。

3. 软引用缓存例子

这个例子的目的如下:

1. 软引用做缓存的一般用法。 2. 验证在垃圾收集后会回收软引用对象。

3.1 代码

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

public class SoftReferenceDemo {
    public static void main(String[] args) {
        OrderManager orderManager = new OrderManager();

        // 获取订单线程,不断获取订单,验证系统内存不足后缓存数据会被清理掉,重新从数据库获取。
        new Thread(()->{
            while (true) {
                orderManager.getOrder("101");
                quietlySleep(2000);
            }
        }).start();

        // 不断创建新对象,模拟内存不足导致垃圾回收,新对象也是软引用,这样可以被回收,避免OOM异常。
        new Thread(()->{
            List<SoftReference<BigObject>> list = new ArrayList<>();
            while (true) {
                list.add(new SoftReference<>(new BigObject()));
                quietlySleep(50);
            }
        }).start();

        // 主线程休眠等待
        quietlySleep(20 * 60 * 1000);
    }

    private static void quietlySleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    // 模拟大对象
    private static class BigObject {
        byte[] b = new byte[4 * 1024];
    }
}

class OrderManager {
    public Order getOrder(String id) {
        Order order = OrderCache.getInstance().getCachedOrder(id);
        if (order == null) {
            order = getOrderFromDB(id);
        }
        return order;
    }

    private Order getOrderFromDB(String id) {
        Order order = new Order(id, (int) (Math.random() * 100));
        System.out.println(new Date() + " get order from DB. XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX " + order);
        OrderCache.getInstance().cache(order);
        return order;
    }
}

class OrderCache {
    private static volatile OrderCache singlonCache;
    private HashMap<String, OrderReference> refChache;
    private ReferenceQueue queue;

    private OrderCache(){
        this.queue=new ReferenceQueue();
        this.refChache=new HashMap<>();
    }

    public static OrderCache getInstance(){
        if(singlonCache == null){
            synchronized (OrderCache.class) {
                if (singlonCache == null) {
                    singlonCache=new OrderCache();
                }
            }
        }
        return singlonCache;
    }

    public void cache(Order order){
        cleanCache();//清除已经标记为垃圾的引用
        OrderReference reference = new OrderReference(order, queue);
        refChache.put(order.getId(), reference);//将对象的软引用保存到缓存中
    }

    public Order getCachedOrder(String key){
        Order order = null;
        if (refChache.containsKey(key)){
            order= (Order) refChache.get(key).get();
            System.out.println(new Date() + " get order from cache. " + order);
        }
        return order;
    }

    private void cleanCache(){
        OrderReference reference = null;
        while ((reference = (OrderReference)queue.poll()) != null){
            System.out.println(new Date() + " cleanCache");
            refChache.remove(reference._key);
        }
    }

    static class OrderReference extends SoftReference {
        private String _key;
        public OrderReference(Order referent, ReferenceQueue q) {
            super(referent, q);
            _key = referent.getId();
        }
    }
}

class Order {
    private String id;
    private long price;
    public Order(String id, long price) {
        this.id = id;
        this.price = price;
    }

    public String getId() {
        return id;
    }

    @Override
    public String toString() {
        return "order id: " + id + ", price: " + price;
    }
}
复制代码

3.2 调整启动参数

调整启动参数内存大小,使得更容易满足内存不足场景。

JAVA基础(二)内存优化-使用Java引用做缓存

3.3 运行输出

Sun Apr 05 17:46:18 GMT+08:00 2020 get order from DB. XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX order id: 101, price: 21
Sun Apr 05 17:46:20 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:22 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:24 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:26 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:28 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:30 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:32 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:34 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:36 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:38 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:40 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:42 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:44 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:46 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:48 GMT+08:00 2020 get order from cache. order id: 101, price: 21
Sun Apr 05 17:46:50 GMT+08:00 2020 get order from cache. null
Sun Apr 05 17:46:50 GMT+08:00 2020 get order from DB. XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX order id: 101, price: 87
Sun Apr 05 17:46:50 GMT+08:00 2020 cleanCache
Sun Apr 05 17:46:52 GMT+08:00 2020 get order from cache. order id: 101, price: 87
Sun Apr 05 17:46:54 GMT+08:00 2020 get order from cache. order id: 101, price: 87
复制代码

可以看到当JVM内存不足,做垃圾回收后软引用会被回收,此时从缓存中无法获得数据,会重新从DB中获取数据。

4. 弱引用例子

4.1 代码

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

public class WeakReferenceDemo {
    public static void main(String args[]) {
        final int size = 3;
        List<WeakReference<DBConnection>> weakList = new ArrayList<WeakReference<DBConnection>>();
        for (int i = 0; i < size; i++) {
            DBConnection dbConnection = new DBConnection("DBConnection-" + i);
            weakList.add(new WeakReference(dbConnection));
            System.out.println(dbConnection + " be created.");
        }
        checkDBConnection(weakList);
        quietlySleep(20000); // 休眠时间调整到你有足够时间在gc之前输入命令 jmap -histo:live <pid> >beforegc.txt,并能在gc之前完成信息收集
        System.gc();                   // 不要通过在这里打断点来执行jmap命令,当暂停到断点时,jmap命令也会暂停执行,断点恢复后,会分不清jsmp收集的是GC前还是GC后的信息
        System.out.println("gc be called.");
        quietlySleep(1000); // 这里占用内存少,很快就回收了,占用内存大的就多给点时间
        checkDBConnection(weakList);   // 这里可以打个断点,以让你知道可以输入命令 jmap -histo:live <pid> >aftergc.txt
        quietlySleep(1000); // 休眠时间调整到在程序退出前有足够时间完成信息收集
    }

    // 检查DBConnection是否被垃圾回收
    private static void checkDBConnection(List<WeakReference<DBConnection>> weakList) {
        for (WeakReference<DBConnection> ref: weakList) {
            DBConnection dbConnection = ref.get();
            System.out.println("dbConnection is null ? " + (dbConnection == null));
        }
    }

    // 让我安静睡会
    private static void quietlySleep(long timeMillis) {
        try {
            Thread.sleep(timeMillis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

// 模拟数据库连接资源
class DBConnection {
    public String id;

    public DBConnection(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }

    @Override
    public String toString() {
        return id;
    }
}
复制代码

4.2 运行后输出日志

DBConnection-0 be created.
DBConnection-1 be created.
DBConnection-2 be created.
dbConnection is null ? false
dbConnection is null ? false
dbConnection is null ? false
gc be called.
dbConnection is null ? true
dbConnection is null ? true
dbConnection is null ? true
复制代码

按照代码中的说明通过jmap命令验证对象是否被回收,在gc之前执行:

jmap -histo 21636 >a.txt
复制代码

注意修改进程号,可通过以下命令查看进程号:

jps |findstr WeakReferenceDemo
复制代码

在GC之后执行:

jmap -histo 21636 >b.txt
复制代码

打开a.txt和b.txt文件查找DBConnection对象,只能在a.txt中找到,而b.txt中找步到,说明确实被回收了。

JAVA基础(二)内存优化-使用Java引用做缓存

4.3 其他例子

在HikariPool中也使用弱引用做缓存,参考 HikariPool源码(二)设计思想借鉴 。

5. 虚引用

没有找到合适的例子和用法。

6. 总结

  1. 在内存敏感的应用中可以使用软引用和弱引用来做缓存,可用根据场景和重要性使用强引用,软引用,弱引用。
  2. 需要考虑缓存不可用时对系统的影响,例如数据库压力增大,做好熔断,降级等措施。

end.

<--感谢三连击,左边点赞和关注。

JAVA基础(二)内存优化-使用Java引用做缓存
原文  https://juejin.im/post/5e8956fcf265da48094d8cfe
正文到此结束
Loading...