《Effective Java》第6條:消除過時的對象引用

原文地址: itweknow.cn/detail?id=6… ,歡迎你們訪問。前端

說到Java,大概不少人都知道GC。Java有自動的垃圾回收機制,固然了,在這篇文章裏面就不去深究GC的具體實現了。那麼之後了垃圾自動回收,咱們是否是就在也不用擔憂內存泄露的問題了呢。這種問題的答案通常來說都是否認的。那麼這篇文章咱們就一塊兒來跟着《Effective Java》來了解一下這個問題。java

過時引用

書中首先提到的就是因爲過時引用而致使的內存泄露,舉了一個Stack的例子:緩存

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];
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}
複製代碼

根據書中的描述,咱們在這個例子中咱們是本身管理內存,垃圾回收器並不清楚element中不活躍的部分能夠被回收,解決這個問題的方式就是咱們經過手動清空這些元素來告訴垃圾回收期它們能夠被回收了。異步

public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    Object obj = elements[--size];
    elements[size] = null;
    return obj;
}
複製代碼

我嘗試了使用以下的代碼,測試了上面有問題的代碼ide

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Stack stack = new Stack();
        for (long i=0; i<Long.MAX_VALUE; i++) {
            doSomething(stack);
        }
    }
    private static void doSomething(Stack stack) {
        for (int i=0; i<5; i++) {
            stack.push(new User(1, "itweknow.cn"));
        }
        for (int i=0; i<5; i++) {
            stack.pop();
        }
    }
}
複製代碼

惋惜並無測出來內存溢出的錯誤,在這裏要感謝https://bbs.csdn.net/topics/391991081這個帖子給了我答案,有興趣的同窗能夠去看一下。再仔細看了一下書中的描述是隨着內存佔用的不斷增長,程序的性能的下降會逐漸表現出來。 在極端的狀況下可能會致使內存溢出。
這麼看來,通常狀況下Stack並不會致使內存溢出,可是因爲失效的元素並不能及時的被GC回收,因此在內存接近零界值的時候會極大的影響程序的性能,因此應該儘可能避免本身管理內存。性能

內存的另外一個泄露來源-緩存

事實上,若是咱們將引用對象放在緩存中,很容易被遺忘,隨着緩存的積累很大機率會出現溢出的可能。比方來說下面這段代碼運行一下就會出現異常。不過如今的電腦廣泛的配置都比較高,因此在運行的時候我設置了一下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);
        }
    }
}
複製代碼

監聽器和其餘回調

在看這條以前我對監聽器和回調接觸的並非不少,因此此次決定研究一撥監聽器卻是是啥。
其實監聽器和回調能夠算是一種異步調用的方式。假設咱們如今有一個提問者有一個問題1+1=?須要問回答者,而後可能回答者計算這個提問須要一段時間。 爲了在單位時間內提問者可以幹更多的事兒,那麼咱們會在提問者類裏實現一個回調接口,而且回答者在問題計算完成的時候調用這個回調方法來告知提問者答案。那麼監聽其實也相似,在咱們實際工做中,特別是前端接觸的可能比較多,好比說按鈕的點擊事件的監聽,給按鈕設置一個監聽器,而後當有點擊事件產生時, 監聽器會調用相應的回調方法進而作相應的處理。下面是咱們這個假設的代碼實現:this

  • 回調接口
/** * @author ganchaoyang * @date 2018/11/15 17:45 * @description 回調接口 */
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就不會去回收被監聽者,長此以往也就有可能會發生內存泄漏。spa

相關文章
相關標籤/搜索