Java/Android Memory Leakを起動するデフォルトのタイプ


OverView


Androidの開発過程で、記憶Leakのリスクに遭遇することがあります.
筆者も何度か面と向かって分析と改善を行ったが,冗長なコードの中で正しい点を見つけるのは容易ではない.
今回のリリースでは、メモリLeakタイプには様々な種類があり、Android StudioのメモリProfilerの使い方を簡単に理解しようとしています.
メモリLeakを開始するコードを記述する前に、Java GC(Garbage Collector)の大まかな操作を理解する必要があります.基本的に、Javaが通常オブジェクトを作成する場合はStrong Referenceであり、オブジェクトが参照されている場合はGCのクリーンアップオブジェクトから離れます.
すなわち、メモリLeakは、割り当てられたメモリを使用しても戻ってこないときに発生し、蓄積時にメモリオーバーフローが発生する.

メモリLeakタイプ


1. Auto Boxing
public class Adder {
       public long addIncremental(long l) {
       		Long sum = 0L;
            sum = sum + l;
            return sum;
       }
       
       public static void main(String[] args) {
			Adder adder = new Adder();
            
            for(long ; i<1000; i++) {
               adder.addIncremental(i);
            }
       }
}
上記の例のコードはprimitive type addIncrementallongパラメータに渡す.
実際の総和を計算するlogicでは、自動起動によりパッケージクラスLongを生成して使用する.
これは,1000回の繰返しごとに不要なオブジェクトが生成され,コードロジックが複雑になると,目で探すことが困難になるためである.つまり、できるだけ元のタイプを使います.
2. Using Cache
public class Cache {
       private Map<String,String> cacheMap = new HashMap<>() {
       		{
              	put("Anil", "Work as Engineer");
            	put("Shamik", "Work as Java Engineer");
            	put("Ram", "Work as Doctor");
            }
       };
       
       public Map<String,String> getCacheMap() {
            return cacheMap;
       }
       
       public void forEachDisplay() {
            for (String key : cacheMap.keySet()) {
                 String val = cacheMap.get(key);
                 System.out.println(key + " :: "+ val);
            }
       }
       
       public static void main(String[] args) {
       		Cache cache = new Cache();
            cache.forEachDisplay();
       }
}
たとえば、Cache classで宣言されたグローバル変数マッピングにデータが格納され、出力されます.
この例では、forEachDisplay以降は使用されないと仮定し、mapの要素がクリアされていないため、アプリケーションはcacheオブジェクトを必要としないが、mapはオブジェクトに強い参照を持っているため、GCができないという問題が発生する.
したがって、独自のCacheエントリが不要になった場合は、WeakHashMapまたはclearを使用します.
3. Improper equals() and hashCode() Implementations
public class Fruit {
    public String name;
    
    public Fruit(String name) {
        this.name = name;
    }
}


@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Fruit, Integer> map = new HashMap<>();
    
    for(int i=0; i<100; i++) {
        map.put(new Fruit("apple"), 1);
    }
    
    Assert.assertFalse(map.size() == 1);
}
この例では、appleというFruitオブジェクトを毎回HashMapに挿入します.
この場合、new Personでオブジェクトを作成すると、そのオブジェクトのhashcodeがそれぞれ異なり、HashMapが異なるキーで挿入されるため、Mapのsizeは100となる.
意図的でない場合、HashMapで使用されるオブジェクトは、equals()メソッドとHashCode()メソッドを再定義する必要があります.
public class Fruit {
    public String name;
    
    public Fruit(String name) {
        this.name = name;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Fruit)) {
            return false;
        }
        Fruit fruit = (Fruit) o;
        return fruit.name.equals(name);
    }
    
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}
4. Closing Connections
try {
  	Connection connection = DriverManager.getConnection();
  	//do something
    connection.close();
} catch (Exception e) {
  	//do something
}
この例では、try blockは接続オブジェクトを作成しますが、Exceptionを起動するとclose()はリソースを解放せず、メモリleckを引き起こす可能性があります.この場合、finallyによってclose()が実行されることを確認しますが、finallyにも例外が発生します.Java 7以降がtry-with-resourceの場合、use scoped関数を使用してリソースを安全に解放します.
5. Internal Data Structure
public class Stack {
       private int maxSize;
       private Object[] stackArray;
       private int pointer;
       
       public Stack(int s) {
              maxSize = s;
              stackArray = newint[maxSize];
              pointer = -1;
       }
       
       public void push(Object element) {
              stackArray[++pointer] = element;
       }
       
       public Object pop() {
              return stackArray[pointer--];
       }
       
       public Object peek() {
              return stackArray[pointer];
       }
       
       public boolean isEmpty() {
              return (pointer == -1);
       }
       
       public boolean isFull() {
              return (pointer == maxSize - 1);
       }
}
この例では、Stack classはこれらの値をObjectarrayとして格納します.
スタックでアクティブ化されている部分はポインタが指す場所で、スタックで()を押してから()をポップアップする過程で問題が発生します.
サンプルコードによれば、1000回プッシュして1000回ポップアップするとpointerの値は0になりますが、内部配列にはすべてのポップアップ参照が含まれます.したがって,すべての要素がポップアップされてもGCの影響を受けることはなく,GCの影響を受けるためにpopにnullを入れて参照を切り離すだけでよい.
public int pop() {
	int size = pointer--
    int element= stackArray[size];
    
    stackArray[size] = null;
    return element;
}
6. Memory Leak Through static Fields
public class StaticTest {
    public static List<Double> list = new ArrayList<>();
  
    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
    }
  
    public static void main(String[] args) {
        new StaticTest().populateList();
    }
}
Javaでは、静的フィールドのライフサイクルはアプリケーションのライフサイクルと同じです.
したがって,アプリケーションが終了するまでリスト内の参照は解放されないためGCターゲットにはならない.
7. Using non-static inner class/anonymous class
public class LeakActivity extends Activity {

  private final Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      	// do something
    }
  }
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    handler.postDelayed(new Runnable() {
      	@Override
      	public void run() {
        	// do something
        }
    }, 60000);
    
    finish();
  }
}
この例ではfinish()を実行し、onCreateが1分後にhandlerを使用してタスクを完了し、Activityを終了するように要求します.
Javaでは、非静的な内部クラスに対して外部クラスへの参照があります.上記のコードでnon-static inner classと宣言されているHandlerは、mainthreadによって作成されたLeakActivityへの参照を持つため、mainthreadのLooperとMessageQueueにバインドされます.
したがって、LeakActivityは終了したにもかかわらず、メッセージキューには遅延したタスクがあり、handlerのLeakActivityへの参照は依然として存在するため、LeakActivityはGCのターゲットにはなりません.
では、どのように修正すればいいのでしょうか.
non-static innerクラスにはouterクラスへの参照があり、static innerクラスにはouterクラスへの参照はありません.また、Handler内部でアクティブなメソッドや変数などのリソースを参照する必要がある場合は、WeakReferenceを使用します.
public class NonLeakActivity extends Activity {
  private NonLeakHandler handler = new NonLeakHandler(this);
  
  private static final class NonLeakHandler extends Handler {
    	private final WeakReference<NonLeakActivity> ref;
    
    	public NonLeakHandler(NonLeakActivity act) {
      		ref = new WeakReference<>(act);  
    	}
    
    	@Override
    	public void handleMessage(Message msg) {
      		NonLeakActivity act = ref.get();
      	 	if (act != null) {
       	       // do something  
      	 	}
    	}
  }
  
  private static final Runnable runnable = new Runnable() {
   		@Override
    	public void run() {
       		// do something  
        }
  }
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    
    	handler.postDelayed(runnable, 60000);
    	finish();
  }
Anonymous classもnon-static inner classと同様にouter classへの参照を持つため、RunnableもAnonymous classからstatic inner classに変更されました.
つまり、Inner Classの場合、Activityのライフサイクルで同じ作成と終了が保証されている場合は、非静的内部クラスとして定義できます.そうでない場合は、静的内部クラスとして定義する必要があります.
6. Using Singletone class
object GlobalSingleton {

	private val listeners = mutableListOf<GlobalSingletonListener>()
    
    fun register(listener: GlobalSingletonListener) {
    	listeners.add(listener)
    }
    
    fun unregister(listener: GlobalSingletonListener) {
    	listeners.remove(listener)
    }
    
    interface GlobalSingletonListener {
    	fun onEvent()
    }
}   
public class LeakActivity extends Activity {

	private val listener = Listener()
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_leaking)
    }
    
    override fun onStart() {
    	super.onStart()
        GlobalSingleton.register(listener)
    }
    
    private inner class Listener : GlobalSingletonListener {
    	override fun onEvent() {
        	// do something
        }
    }
}
この例では、LeakActivityでグローバル単一ポイントListenerを実装するListenerをグローバル単一ポイントオブジェクトに登録します.
Singletonクラスは静的クラスであるため、アプリケーションと同じライフサイクルを有します.すなわち,LeakActivityが終了しても,リスナーのインスタンスはグローバルSingletonのGC回収ターゲットにはならない.

整理する


簡単な記憶を起こしがちなLeakの例を見てみましょう.
繰り返しますが、8つの例の共通点は、オブジェクト参照から開発者がそのオブジェクトのライフサイクルを知ることです.
GC到達領域に入るためには、使用済みのオブジェクトが解除されることに注意してください.
  • https://dzone.com/articles/memory-leak-andjava-code
  • https://medium.com/@joongwon/android-memory-leak-%EC%82%AC%EB%A1%80-6565b817a8fe
  • https://medium.com/@joongwon/android-memory-leak-%EC%82%AC%EB%A1%80-6565b817a8fe
  • https://www.youtube.com/watch?v=4Wnu_2meZaI