1.利用软引用和弱引用解决OOM问题
假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存中,又可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
2.使用软引用构建敏感数据的缓存
假如有一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询缓存在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按钮)。
雇员信息类:
public class Employee {
/**
* 雇员的标识码
*/
private String id;
/**
* 雇员姓名
*/
private String name;
/**
* 该雇员所在部门
*/
private String department;
/**
* 该雇员联系电话
*/
private String phone;
/**
* 该雇员薪资
*/
private int salary;
/**
* 该雇员信息的来源
*/
private String origin;
public Employee(String id){
this.id = id;
getDataFromInfoCenter();
}
/**
* 从数据库中获取雇员信息
*/
private void getDataFromInfoCenter(){
//和数据库建立连接并查询该雇员的信息,将查询结果赋值
//给name、department、phone、salary等变量赋值
//同时将origin赋值为“from database”
this.name = "name" + id;
this.department = "department" + id;
this.phone = "phone" + id;
this.salary = new Random().nextInt(101);
this.origin = "from database";
}
public String getId() {
return id;
}
public void setOrigin(String origin) {
this.origin = origin;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Employee{");
sb.append("id='").append(id).append('\'');
sb.append(", name='").append(name).append('\'');
sb.append(", department='").append(department).append('\'');
sb.append(", phone='").append(phone).append('\'');
sb.append(", salary=").append(salary);
sb.append(", origin='").append(origin).append('\'');
sb.append('}');
return sb.toString();
}
}
雇员缓存类:
public class EmployeeCache {
/**
* 一个Cache实例
*/
private static EmployeeCache cache;
/**
* 用于cache内容的存储
*/
private Hashtable<String, EmployeeRef> employeeRefs;
/**
* Reference队列
*/
private ReferenceQueue<Employee> queue;
/**
* 继承SoftReference,使得每一个实例都具有可识别的标识
* 并且该标识与其在HashMap内的key相同
*/
private class EmployeeRef extends SoftReference<Employee>{
/**
* 实例的可识别标识
*/
private String key = "";
public EmployeeRef(Employee employee, ReferenceQueue<Employee> queue){
super(employee, queue);
key = employee.getId();
}
}
/**
* 构建一个缓存器实例
*/
private EmployeeCache(){
employeeRefs = new Hashtable<>();
queue = new ReferenceQueue<>();
}
/**
* 获取缓存器实例
* @return 缓存器实例
*/
public static EmployeeCache getInstance(){
if(null == cache){
cache = new EmployeeCache();
}
return cache;
}
/**
* 以软引用的方式堆对一个Employee对象的实例进行引用并保存该引用
* @param employee
*/
private void cacheEmployee(Employee employee){
//清除垃圾引用
cleanCache();
EmployeeRef ref = new EmployeeRef(employee,queue);
employeeRefs.put(employee.getId(),ref);
}
/**
* 根据所指定的ID号码,重新获取相应的Employee对象的实例
* @param id 雇员的标识码
* @return 雇员的实例对象
*/
public Employee getEmployee(String id){
Employee employee = null;
if(employeeRefs.containsKey(id)){
EmployeeRef ref = employeeRefs.get(id);
employee = ref.get();
employee.setOrigin("from cache");
}
//如果没有软引用,或者从软引用中得到的实例时null,重新构建一个实例
//保存对这个新建实例的软引用
if(null == employee){
employee = new Employee(id);
System.out.println("Retrieve From EmployeeInfoCenter.id = " + id);
this.cacheEmployee(employee);
}
return employee;
}
/**
* 清除软引用的Employee对象已经被回收的EmployeeRef对象
*/
private void cleanCache(){
EmployeeRef ref = null;
while(null != (ref = (EmployeeRef) queue.poll())){
employeeRefs.remove(ref);
}
}
/**
* 清除Cache内的全部内容
*/
public void clearCache(){
cleanCache();
employeeRefs.clear();
System.gc();
System.runFinalization();
}
}
雇员信息测试启动类:
public class EmployeeTest {
public static void main(String[] args){
//第一次获取1111的对象
Employee employee1 = EmployeeCache.getInstance().getEmployee("1111");
System.out.println(employee1);
//第一次获取2222的对象
Employee employee2 = EmployeeCache.getInstance().getEmployee("2222");
System.out.println(employee2);
//第二次获取1111的对象
Employee employee3 = EmployeeCache.getInstance().getEmployee("1111");
System.out.println(employee3);
//将1111对象的强引用全部清除,只留2222对象的强引用
employee1 = null;
employee3 = null;
//清除全部软引用
System.out.println("------------清除全部软引用--------------");
EmployeeCache.getInstance().clearCache();
//重新获取1111的对象,发现需要重新查库
Employee employee4 = EmployeeCache.getInstance().getEmployee("1111");
System.out.println(employee4);
//重新获取2222的对象,发现也需要重新查库
// 这说明清除全部软引用的时候,哪怕还有强引用,也会被清除
Employee employee5 = EmployeeCache.getInstance().getEmployee("2222");
System.out.println(employee5);
}
}
打印结果为:
Retrieve From EmployeeInfoCenter.id = 1111
Employee{id='1111', name='name1111', department='department1111', phone='phone1111', salary=61, origin='from database'}
Retrieve From EmployeeInfoCenter.id = 2222
Employee{id='2222', name='name2222', department='department2222', phone='phone2222', salary=41, origin='from database'}
Employee{id='1111', name='name1111', department='department1111', phone='phone1111', salary=61, origin='from cache'}
------------清除全部软引用--------------
Retrieve From EmployeeInfoCenter.id = 1111
Employee{id='1111', name='name1111', department='department1111', phone='phone1111', salary=68, origin='from database'}
Retrieve From EmployeeInfoCenter.id = 2222
Employee{id='2222', name='name2222', department='department2222', phone='phone2222', salary=13, origin='from database'}
3.使用弱引用构建非敏感数据的缓存
无意识对象保留最常见的原因时使用Map将元数据与临时对象(transient object)相关联。假定一个对象具有中等生命周期,比分配的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。在创建Socket时是不知道这些西悉尼的,并且不能将数据添加到Socket对象上,因为不能控制Socket类或者它的子类。这时,典型的方法就是在一个全局Map中存储这些信息。
public class SocketManager {
private Map<Socket, User> socketUserMap = new HashMap<>();
public void setUser(Socket socket, User user){
socketUserMap.put(socket, user);
}
public User getUser(Socket socket){
return socketUserMap.get(socket);
}
public void removeUser(Socket socket){
socketUserMap.remove(socket);
}
}
这种方法的问题与元数据的生命周期挂钩,除非准确地知道什么时候程序不再需要这个套接字,并记住从Map中删除相应的映射,否则,Socket和User对象将永远留在Map中,远远超过了响应了请求和关闭套接字的时间。这会阻止Socket和User对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。
在Java集合中有一种特殊的Map类型----WeakHashMap。在这种Map中存放了键对象的弱引用,当一个键对象被垃圾回收器回收时,那么相应的值对象的引用会从Map中删除。WeakHashMap能够节约存储空间,可用来缓存那些非必须存在的数据。
Element模型类
public class Element {
private String id;
public Element(String id){
this.id = id;
}
@Override
public int hashCode(){
return id.hashCode();
}
@Override
public boolean equals(Object obj){
return obj instanceof Element && id.equals(((Element) obj).id);
}
@Override
protected void finalize(){
System.out.println("Finalizing " + getClass().getSimpleName() + id);
}
@Override
public String toString(){
return id;
}
}
class Key extends Element{
public Key(String id){
super(id);
}
}
class Value extends Element{
public Value(String id){
super(id);
}
}
启动类
public class CanonicalMapping {
public static void main(String[] args){
int size = 10;
Key[] keys = new Key[size];
WeakHashMap<Key, Value> map = new WeakHashMap<>(32);
for(int i = 0 ; i < size; i++){
Key key = new Key(Integer.toString(i));
Value value = new Value(Integer.toString(i));
if(i % 3 == 0){
//3的整数key增加强引用
keys[i] = key;
}
map.put(key, value);
}
System.out.println("未触发GC前:" + map);
System.gc();
System.out.println("触发GC后" + map);
}
}
打印结果为:
未触发GC前:{8=8, 9=9, 4=4, 5=5, 6=6, 7=7, 0=0, 1=1, 2=2, 3=3}
Finalizing Key2
Finalizing Key8
Finalizing Key7
Finalizing Key5
Finalizing Key4
Finalizing Key1
触发GC后{9=9, 6=6, 0=0, 3=3}
从打印结果可以看出,没有增加强引用的键值对在触发了GC之后全部被回收了。