天天看點

【轉載】Java中如何寫一段記憶體洩露的程式 & ThreadLocal 介紹和使用

可以參考這段文章:

​​link​​

A1:通過以下步驟可以很容易産生記憶體洩露(程式代碼不能通路到某些對象,但是它們仍然儲存在記憶體中):

上文中提到了使用ThreadLocal造成了記憶體洩露,但是寫的不清不楚,簡直不是人寫的文字,太差了。。。用另一篇清晰的文章來解釋吧:

http://www.cnblogs.com/onlywujun/p/3524675.html

如下圖,實線代表強引用,虛線代表弱引用.:

【轉載】Java中如何寫一段記憶體洩露的程式 & ThreadLocal 介紹和使用

每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal執行個體.      
這個Map的确使用了弱引用,不過弱引用隻是針對key. 每個key都弱引用指向threadlocal.      
當把threadlocal執行個體置為null以後,沒有任何強引用指向threadlocal執行個體,是以threadlocal将會被gc回收.      
但是,我們的value卻不能回收,因為存在一條從current thread連接配接過來的強引用.      
隻有目前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value将全部被GC回收.

是以得出一個結論就是隻要這個線程對象被gc回收,就不會出現記憶體洩露,但在threadLocal設為null和線程結束這段時間不會被回收的,      
就發生了我們認為的記憶體洩露。其實這是一個對概念了解的不一緻,也沒什麼好争論的。      
最要命的是線程對象不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用線程池的時候,線程結束是不會銷毀的,會再次使用的。      
就可能出現記憶體洩露。  

PS.Java為了最小化減少記憶體洩露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map裡所有key為null的value。      
是以最怕的情況就是,threadLocal對象設null了,開始發生“記憶體洩露”,然後使用線程池,這個線程結束,線程放回線程池中不銷毀,      
這個線程一直不被使用,或者配置設定使用了又不再調用get,set方法,那麼這個期間就會發生真正的記憶體洩露。      

先了解一下ThreadLocal類提供的幾個方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }      

使用的例子:

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();


    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();


        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());


        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();

        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}      

輸出結果:

已進行驗證:

1
main
10
Thread-0
1
main      

在main線程中,如果沒有先set,直接get的話,運作時會報空指針異常。但是如果改成下面這段代碼,即重寫了initialValue方法:

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
        protected Long initialValue() {
            return Thread.currentThread().getId();
        };
    };
    ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;
        protected String initialValue() {
            return Thread.currentThread().getName();
        };
    };


    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();

        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());


        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();

        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}      

以上運作正常。

ThreadLocal的應用場景

最常見的ThreadLocal使用場景為 用來解決 資料庫連接配接、Session管理等。如:

private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
}
};

public static Connection getConnection() {
return connectionHolder.get();
}      

A2:JVM的GC不可達區域,比如通過native方法配置設定的記憶體。

A3:如果HashSet未正确實作(或者未實作)hashCode()或者equals(),會導緻集合中持續增加“副本”。如果集合不能地忽略掉它應該忽略的元素,它的大小就隻能持續增長,而且不能删除這些元素。

如果你想要生成錯誤的鍵值對,可以像下面這樣做:

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.      

以下的,感覺比較瑣碎。有時間再研究。

A4:除了被遺忘的監聽器,靜态引用,hashmap中key錯誤/被修改或者線程阻塞不能結束生命周期等典型記憶體洩露場景,下面介紹一些不太明顯的Java發生記憶體洩露的情況,主要是線程相關的。

  • 當ThreadGroup自身沒有線程但是仍然有子線程組時調用ThreadGroup.destroy()。發生記憶體洩露将導緻該線程組不能從它的父線程組移除,不能枚舉子線程組。
  • 使用WeakHashMap,value直接(間接)引用key,這是個很難發現的情形。這也适用于繼承Weak/SoftReference的類可能持有對被保護對象的強引用。