Effective Java Item6-消除作廢的對象引用

Effective Java 2nd Edition Reading Notes

Item6: Eliminate Obsolete Object References

消除作廢的對象引用。

 

C或者C++中需要程序員自己進行內存管理,而Java通過使用GC來自動管理不再使用的對象。但是在有些時候,顯式的消除廢棄的對象應用是必要的。

例如在下面的代碼中,StackInRisk類中的pop方法並沒有將pop的對象引用消除刪除,從而導致即使在其他的地方不再引用pop出去的對象(stack增大後再縮小),該對象仍然不會被GC回收。這樣將導致性能的下降,甚至發生內存溢出異常(雖然這種情況非常罕見)

package com.googlecode.javatips4u.effectivejava.eliminate;

import java.util.EmptyStackException;

public class StackInRisk {

       private Object[] elements;

       private int size = 0;

       private static final int DEFAULT_INITIAL_CAPACITY = 16;

       public StackInRisk() {

              elements = new Object[DEFAULT_INITIAL_CAPACITY];

       }

       public void push(Object e) {

              ensureCapacity();

              elements[size++] = e;

       }

       public Object pop() {

              if (size == 0) {

                     throw new EmptyStackException();

              }

              // memory leak here, did not remove the object in the elements array.

              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) {

                     Object[] elementsNew = new Object[size * 2 + 1];

                     System.arraycopy(elements, 0, elementsNew, 0, size);

                     elements = elementsNew;

              }

       }

}

這是由於Stack維持了對這些對象的作廢的引用。作廢的引用是指不會被解除的引用。一個被作廢引用的對象不僅僅不會被GC回收,該對象引用的其他對象也不會被GC回收,依次類推。所以有時即使只是一部分對象引用是作廢引用,那麼將導致非常多的對象不能被回收。

上面對Stack解決內存泄漏問題的方法是將pop出去的對象解引用。

//     public Object pop() {

//            if (size == 0)

//                   throw new EmptyStackException();

//            Object result = elements[--size];

//            elements[size] = null; // Eliminate obsolete reference

//            return result;

//     }

顯式的將該對象引用設置爲null的好處在於,如果程序再次使用該引用進行操作,那麼將會拋出NullPointerException,而不是什麼都不做。

 

但是並不是什麼時候都需要進行顯式的null操作。顯式的null化應該是特定的情況才進行的,而不是正常的情況下使用。(畢竟GC可以做絕大部分的操作)。最好的消除廢棄引用的辦法是:使包含引用的變量超出作用域,通過在最小的範圍內定義變量引用。

[通過將變量定義在最小的作用域範圍內,可以增強代碼的可讀性和可維護性]

那麼到底什麼時候需要進行顯式的null化操作呢?

一般說來,當一個類本身進行內存的管理操作時,例如上面提到的Stack。需要注意內存泄漏。

另外一個內存泄漏的情況是緩存。當將一個對象放入緩存中,很容易忘記它的存在,直到它變得無關。

一般的解決方式是使用WeakHashMap來表示Cache

public class WeakHashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>

A hashtable-based Map implementation with weak keys. An entry in a WeakHashMap will automatically be removed when its key is no longer in ordinary use. More precisely, the presence of a mapping for a given key will not prevent the key from being discarded by the garbage collector, that is, made finalizable, finalized, and then reclaimed. When a key has been discarded its entry is effectively removed from the map, so this class behaves somewhat differently than other Map implementations.

This class is intended primarily for use with key objects whose equals methods test for object identity using the == operator. Once such a key is discarded it can never be recreated, so it is impossible to do a lookup of that key in a WeakHashMap at some later time and be surprised that its entry has been removed. This class will work perfectly well with key objects whose equals methods are not based upon object identity, such as String instances. With such recreatable key objects, however, the automatic removal of WeakHashMap entries whose keys have been discarded may prove to be confusing.

WeakHashMap實現了Map接口,但是它的value會在key不使用的時候自動的移除。也就是說,對於一個指定的keymap不會阻止它被GC回收,即通過finalizable->finalize->reclaimed。當key被回收後,value也會從map中移除。

但是該類是主要用於比較兩個對象是否equals時,通過對象的標誌即==運算來比較的。如果不是通過==來比較equals,例如String,那麼String是可以被重新創建的(equals返回true的兩個對象),那麼WeakHashMap是不會自動刪除的。示例代碼:

package com.googlecode.javatips4u.effectivejava.eliminate;

import java.io.File;

public class CacheTest {

       private static final File windows = new File("C:/windows");

       private static final File system = new File("C:/windows/system32");

       // THESE KEYS MAY BE USED SOMEWHERE ELSE

       public static final CacheKey WINDOW_KEY = new CacheKey(1, "windows");

       public static final CacheKey SYSTEM_KEY = new CacheKey(2, "system");

       public static void main(String[] args) {

              CacheTest cachetest = new CacheTest();

              cachetest.test();

              CacheKey windowKey = new CacheKey(1, "windows");

              CacheKey systemKey = new CacheKey(2, "system");

              System.out.println("because the windowsKey and systemKey were"

                            + " discarded by the garbage collector, "

                            + "obtaining cache return null.");

              System.out.println(Cache.obtain(windowKey));

              System.out.println(Cache.obtain(systemKey));

              System.out.println("because the WINDOW_KEY and SYSTEM_KEY were"

                            + " not discarded by the garbage collector, "

                            + "obtaining cache return cached object.");

              System.out.println(Cache.obtain(WINDOW_KEY));

              System.out.println(Cache.obtain(SYSTEM_KEY));

              System.out.println("because the windows and system string were"

                            + " recreatable,obtaining cache return cached object.");

              System.out.println(Cache.obtainRecreatable("windows"));

              System.out.println(Cache.obtainRecreatable("system"));

              System.out.println("END");

       }

       public void test() {

              CacheKey windowKey = new CacheKey(1, "windows");

              CacheKey systemKey = new CacheKey(2, "system");

              Cache.cache(windowKey, windows);

              Cache.cache(systemKey, system);

              Cache.cache(WINDOW_KEY, windows);

              Cache.cache(SYSTEM_KEY, system);

              Cache.cacheRecreatable("windows", windows);

              Cache.cacheRecreatable("system", system);

              System.out.println("because the keys are not being discarded"

                            + " by the garbage collector, "

                            + "obtaining cache return cached objects.");

              System.out.println(Cache.obtain(windowKey));

              System.out.println(Cache.obtain(systemKey));

              System.out.println(Cache.obtain(WINDOW_KEY));

              System.out.println(Cache.obtain(SYSTEM_KEY));

              System.out.println(Cache.obtainRecreatable("windows"));

              System.out.println(Cache.obtainRecreatable("system"));

       }

}

上面的代碼的控制檯輸出如下:

because the keys are not being discarded by the garbage collector, obtaining cache return cached objects.

C:/windows

C:/windows/system32

C:/windows

C:/windows/system32

C:/windows

C:/windows/system32

because the windowsKey and systemKey were discarded by the garbage collector, obtaining cache return null.

null

null

because the WINDOW_KEY and SYSTEM_KEY were not discarded by the garbage collector, obtaining cache return cached object.

C:/windows

C:/windows/system32

because the windows and system string were recreatable,obtaining cache return cached object.

C:/windows

C:/windows/system32

END

更普遍的情況是Cache的對象的生命週期沒有進行正式的定義,那麼隨着時間的推移,cache的對象的價值在降低,在這種情況下,需要在對Cache進行清理。這可以通過後臺線程或者在新加入entry的時候移除最舊的對象。這可以通過LinkedHashMapremoveEldestEntry來實現。

Returns true if this map should remove its eldest entry. This method is invoked by put and putAll after inserting a new entry into the map. It provides the implementer with the opportunity to remove the eldest entry each time a new one is added. This is useful if the map represents a cache: it allows the map to reduce memory consumption by deleting stale entries.

Sample use: this override will allow the map to grow up to 100 entries and then delete the eldest entry each time a new entry is added, maintaining a steady state of 100 entries.

 

     private static final int MAX_ENTRIES = 100;

 

     protected boolean removeEldestEntry(Map.Entry eldest) {

        return size() > MAX_ENTRIES;

     }

 

This method typically does not modify the map in any way, instead allowing the map to modify itself as directed by its return value. It is permitted for this method to modify the map directly, but if it does so, it must return false (indicating that the map should not attempt any further modification). The effects of returning true after modifying the map from within this method are unspecified.

This implementation merely returns false (so that this map acts like a normal map - the eldest element is never removed).

樣例代碼:

package com.googlecode.javatips4u.effectivejava.eliminate;

import java.io.File;

import java.util.LinkedHashMap;

public class LinkedHashMapCache {

       private static LinkedHashMap<String, File> cache = new LinkedHashMap<String, File>() {

              private static final int MAX_SIZE = 5;

              protected boolean removeEldestEntry(

                            java.util.Map.Entry<String, File> eldest) {

                     if (size() > MAX_SIZE) {

                            return true;

                     }

                     return false;

              };

       };

       public static void cache(String key, File value) {

              cache.put(key, value);

       }

       public static File obtain(String key) {

              return cache.get(key);

       }

       public static void main(String[] args) {

              File entry = new File("c:/");

              LinkedHashMapCache.cache("a", entry);

              LinkedHashMapCache.cache("b", entry);

              LinkedHashMapCache.cache("c", entry);

              LinkedHashMapCache.cache("d", entry);

              LinkedHashMapCache.cache("e", entry);

              System.out.println(LinkedHashMapCache.cache.size());

              LinkedHashMapCache.cache("f", entry);

              System.out.println(LinkedHashMapCache.cache.size());

       }

}

控制檯輸出:

5

5

 

另一個內存泄漏的情況是監聽器和其他的回調。當實現了一個監聽器,客戶端可以註冊回調函數,但是沒有取消註冊,那麼這些對象就會積累,除非api中採取一些動作。確保回調對象被GC的最好方法就是使用weak reference來存儲對回調對象,例如將它們作爲WeakHashMapkey

此情況與上述的WeakHashMap類似。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章