《Effective Java》阅读笔记6 消除过期的对象引用

·  阅读 193

1.序

如果有同学接触过C/C++,应该会对内存空间的回收有着深刻的印象。举一个典型的例子——==删除C语言链表==。

void deleteList(list l){
    position p, temp;​
    p = l->next;
    l->next = NULL;
    while(p != NULL)
    {
        temp = p->next;
        free(p); // 释放掉结点占用的内存空间        
        p = temp;
    }}
复制代码

不懂C语言的同学没有必要弄清楚这段代码的具体含义,只需要注意那一行被注释的代码就行了。我们在删除一个链表时,必须释放掉链表结点所占用的内存空间,否则这段空间会一直处于被占用的状态,同时我们也无法再次使用它,这样的内存空间就成为了垃圾。这种情况称为内存泄漏。(C++同理,必须手动回收垃圾对象)

但显然,我们在Java中没有使用过类似的操作,那是因为JVM自带垃圾回收(Garbage Collection,简称GC)机制,会自动回收那些我们不再使用的对象。关于JVM的GC机制的具体原理,大家可以参考这篇云栖社区的文章。我就不再赘述了。

参考链接---https://zhuanlan.zhihu.com/p/73628158

2.过期引用

因为既然是自己管理内存,那么对于内存的释放当然也要自己注意。看下面的一个类。

public class Stack {
	private Object[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;

	public Stack() {
		elements = new Object[DEFAULT_INITIAL_CAPACITY];
	}

	public void push(Object e) {
		ensureCapacity();
		elements[size++] = e;
	}

	public Object pop() {
		if (size == 0)
			throw new EmptyStackException();
		return elements[--size];
	}

	/**
	 * Ensure space for at least one more element, roughly doubling the capacity
	 * each time the array needs to grow.
	 */
	private void ensureCapacity() {
		if (elements.length == size)
			elements = Arrays.copyOf(elements, 2 * size + 1);
	}
}
复制代码

对于pop方法,一个单元弹出栈后,指向这个单元的引用就过期了,需要清空该引用。改进的方法非常简单,对于该引用指向null即可。

public Object pop() {
		if (size == 0)
			throw new EmptyStackException();
		Object result = elements[--size];
		elements[size] = null;
		return result;
	}
复制代码

当然关于清空对象的引用不用过分地太注意,正如文中说的:清空对象引用只是一种例外,不是一种规范行为。

3.内存的另一个泄露来源-缓存

一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。随着缓存的积累很大概率会出现溢出的可能。比方来讲下面这段代码运行一下就会出现异常。不过现在的电脑普遍的配置都比较高,所以在运行的时候我设置了一下JVM参数。

public class Question {
    /**
     * 假如我们现在有一个需求,需要在java内存中缓存发送给用户的短信验证码
     * 这里使用HashMap来存储,测试当缓存达到1000000的时候是否会发生内存泄露。
     * 启动时设置最大堆内存为10M
     * @param args
     */
    public static void main(String[] args) throws InterruptedException {
        HashMap<Integer, String> codeCache = new HashMap<Integer, String>();
        // 方便起见,使用固定验证码。
        String code = "000000";
        for (int i=0; i<1000000; i++) {
            codeCache.put(i, code);
            Thread.sleep(1);
        }
    }
}
复制代码

对于这个问题有几种解决方案。如果你正好想实现了一个缓存:只要在缓存之外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就可以用==WeakHashMap==来表示缓存;

这些项在过期之后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,WeakHashMap才有用。

更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程(也许是ScheduledThreadPoolExecutor)或将新的项添加到缓存时顺便清理。LinkedHashMap类使用它的removeEldestEntry方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用java.lang.ref。

4.监听器和其他回调

在看这条之前我对监听器和回调接触的并不是很多,所以这次决定研究一拨监听器倒是是啥。 其实监听器和回调可以算是一种异步调用的方式。假设我们现在有一个提问者有一个问题1+1=?需要问回答者,然后可能回答者计算这个提问需要一段时间。 为了在单位时间内提问者能够干更多的事儿,那么我们会在提问者类里实现一个回调接口,并且回答者在问题计算完成的时候调用这个回调方法来告知提问者答案。那么监听其实也类似,在我们实际工作中,特别是前端接触的可能比较多,比如说按钮的点击事件的监听,给按钮设置一个监听器,然后当有点击事件产生时, 监听器会调用相应的回调方法进而做相应的处理。下面是我们这个假设的代码实现:

回调接口

public interface CallBack {

    /**
     * 知道答案的回调接口
     * @param result
     */
    public void solve(String result);

}
复制代码

提问者

public class Questioner implements CallBack{

    /**
     * 提问者的姓名
     */
    private String name;

    /**
     * 持有一个问题回答者,因为需要知道向谁提问
     */
    private Author author;

    public Questioner(String name) {
        this.name = name;
    }

    /**
     * 设置提问者,相当于设置监听器
     * @return
     */
    public Questioner setAuthor(Author author) {
        this.author = author;
        return this;
    }

    public void doRequest() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                author.execRequest(Questioner.this, "question");
            }
        }).start();
        // 问题问了之后,去做其他事情。
        doOtherThings();
    }

    private void doOtherThings() {
        System.out.println("do other things...");
    }

    /**
     * 回答者知道答案后会通过这个方法来告知提问者答案
     * @param result
     */
    @Override
    public void solve(String result) {
        System.out.println(author.getName() + "告诉" + getName()
                + "答案是:" + result);
    }

    public String getName() {
        return name;
    }

    public Questioner setName(String name) {
        this.name = name;
        return this;
    }
}
复制代码

回答者

public class Author {

    private String name;

    public Author(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public Author setName(String name) {
        this.name = name;
        return this;
    }

    /**
     * 处理问题
     * @param callBack
     * @param qeustion
     */
    public void execRequest(CallBack callBack, String qeustion) {
        // 有一个等待,模拟处理过程。
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 得到答案,调用提问者的回调方法。
        callBack.solve("solved");
    }
}
复制代码

调用方式

public class Main {

    public static void main(String[] args) throws InterruptedException {
        // 假设现在有10000个提问者,10000个回答者,提问者分别向回答者提问。
        for (int i=0; i< 10000 ; i++) {
            Questioner questioner = new Questioner("Q" + i);
            Author author = new Author("A" + i);
            questioner.setAuthor(author);
            questioner.doRequest();
            Thread.sleep(100);
        }
    }
}
复制代码

我们给被监听者设置了一个监听器,如果我们在被监听者销毁的时候没有去注销监听,那么监听器就会一直持有被监听者的引用,这个时候GC就不会去回收被监听者,久而久之也就有可能会发生内存泄漏

5.参考文献

my.oschina.net/silence88/b… zhuanlan.zhihu.com/p/115086322

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享

7.你们要的免费书来了

分类:
后端
标签:
分类:
后端
标签: