メモリよ
飛び放て!
ガベージコレクションをマスターし
自在にメモリを管理する

image: メモリよ飛び放て!
Toplabo
Garbage Collection Explore

ガベージコレクションで不要なメモリを解放し
快適なアプリ体験を提供

 | 2025/03/31

発端

メモリ管理が重要なのはわかっちゃいるが・・・
  • オリジナルの画像検索アプリがめっちゃ重い

    ここのプロジェクトでも何度か紹介しているオリジナルの画像表示WEBアプリ。
    大量の画像を表示させ使えそうな写真を探すブラウザベースのWEBアプリで、かなり使用頻度が高いのですが・・・。
    以前からずっと感じてはいたんですが、firefoxがめちゃくちゃ重くなるんですね。
    最初はサクサクなんですが、だんだんブラウザどころかPC自体の動作が凄く重くなるんですよ。
    面倒そうなのでこれまではなんとか頭の隅においやっていたんですが、観念しました。
    重さの原因を究明することにしました。
    まあ、うすうす原因はメモリ関連というのは想像していたし、たぶん画像の管理方法に問題があるというのは思ってました。

原因
究明

適切なメモリ管理が必要なようだ
  • 画像のメモリがどんどん増えていく

    まず、メモリの使用状況を調べるためにfirefoxの開発ツール、メモリタブで調べてみることに。
    メモリタブの統計リストで要素のメモリ使用状況を調べることができます。で、A要素(IMG要素の画像ではなくA要素の背景画像にしているため)を見ると使用量ががどんどん増えていくのを確認。これじゃあ、重くなるはずだ!
    つまり画像を読み込んでも解放せずにどんどんメモリに画像データをため込んでいってる状況です。
    このままではいかん!ということでメモリの管理を徹底することに!

メモリ
管理

メモリ管理のことをガベージコレクションというらしい
  • ガベージコレクションとは

    ガベージコレクションとは、不要になったメモリを自動で解放する仕組みのことです。
    JavaScriptの実行環境(ブラウザやNode.js)は、このGCを使ってメモリ管理をしているらしい。
    しかもfirefoxのメモリ管理はかなり優秀らしい・・・ホントか?

    どのようにメモリを解放するのか?

    調べたところ、avaScriptのガベージコレクションは、主に「到達可能性(Reachability)」を基準に動作するらしいです。
    要は、現在のプログラムからアクセスできないオブジェクトは「不要」と判断され、GCが回収する仕組み、らしい。
    とはいううものの、メモリ管理を自発的にやっていくためには、不要と判断される基準は何か、など解放される条件を正確に調べておく必要があります。
    さらに、すべてのメモリが即座に解放されるわけではなく、GCがいつ発動するかはエンジンの判断にし、これがメモリリークの原因になることもあるそうで、これもメモリ管理をブラウザに任せっぱなしにしてはいけない理由になります。
    というわけでメモリ管理の悪い例と良い例をサンプルにメモリ管理の方法を調べてみようと思います。

良い例
悪い例

メモリ管理の良い例と悪い例を見てみる
  • 悪い例

    まずは悪い例です。例の問題ありのWEBアプリを単純化しました。
    プログラムの概要は、特定の画像フォルダを走査し、画像ファイルだけを取得し、ブラウザで決まった枚数づつ表示させます。
    いわゆる画像ビュアーです。画像の枚数は、普段は数万点を扱いますが今回は検証しやすい様に5点にしました。

    import { getData } from './modules/getData.js';
    
    const i = {
    	_data: [],
    	get data(){return this._data},
    	set data(v){this._data= v},
    	imgObjects: [],
    	programuPath: '../',
    	thumbdir: 'images',
    	extension: 'webp',
    	displayCount: 2
    };
    
    async function setready() {
    	await getData(i);
    	changeImg();
    }
    setready();
    /* ここまでで画像フォルダから画像ファイルを収集 */
    
    const gallery= document.querySelector('#gallery');
    const button= document.querySelector('button');
    button.addEventListener('click', changeImg, false);
    let num= 0;
    
    /* 画像変更ボタンを押した時に起動するchangeImg関数 */
    function changeImg(){
    	const totalImgCount= i.data.length;
    	const totalPageCount= Math.ceil( totalImgCount/i.displayCount );
    
    	let imgtags= [];
    	if(num>= totalPageCount){
    		num= 0;
    	}
    
    /* i.data配列の中に画像ファイル名が格納されている 
     * 表示する数だけIMG要素を作成し画像の配列へ格納し
    */
    
    	i.data.forEach(function(v, k){
    		if(i.displayCount*num <= k && k < i.displayCount*(num +1 )){
    			imgtags.push( `<img src="../images/${v}" alt="images/${v}">`);
    		}
    	});
    	num++;
    /* 
     * 配列のすべての要素を連結して変数に変換。
     * それをID=galleryの要素内へ入れ替え。
     */
    	gallery.innerHTML = imgtags.join('');
    }
    
    メモリリークが発生する実装見本

    メモリリークが発生する実装見本:https://labo.studio-happyvalley.com/garbage_collection/bad/

    開発ツールで調べてみると問題点が明確に

    firefoxの開発ツールで調べてみるとたった5点しかないはずの画像が、メモリ内でどんどん増殖していくのを確認。

    https://studio-happyvalley.com/wp/wp-content/uploads/gc_000.png最初は画像を2点表示するだけなので、画像は2個表示されている。https://studio-happyvalley.com/wp/wp-content/uploads/gc_001.pngところが、画像変換を繰り返すと、画像は5点しかないはずなのに、メモリには5個以上保存されている
    原因究明

    5点しかないはずの画像を繰り返し表示させると、どんどん画像がメモリに蓄積されていく。
    つまり画像のメモリ解放が正しくおこなわれていないということ。
    どうすれば画像のメモリを解放できるのか、それには画像の参照を切ればメモリから解放されるということなので、まずは表示が済んだ画像や要素は即削除することに
    また、gallery.firstChild.src = ""; で明示的に画像の参照を切ったり、不要な配列や変数の参照を解除したり・・・。
    が、それでも変わらず、メモリは増え続ける。
    それならとgallery内に子要素を追加する前に必ずクリアにする、innerHTMLを.replaceChildrenに変更したり、while ループで明示的に削除するなどいろいろ試すが、まったく効果なし。

    画像のメモリ管理を確実にする

    現状は画像は直接、img要素に記述してからgallery要素内へ追加しているが、それをいったん画像のみをメモリ内へプールし、表示させるべき順番がきたら保存しているが画像を共有し表示させる仕様に変更。
    つまり5点の画像をメモリに保存してしまい、必要な時に差し替えるようにすれば、画像メモリは5個以上にはならないという考え。
    そのサンプルが以下。

    画像を共有前提の実装見本:https://labo.studio-happyvalley.com/garbage_collection/savememory/

    画像のメモリリークは解消しているようだ

    今度のコードは画像が5個以上メモリに残らないように動作しているかを確認するのがポイントなので、画像変換ボタンを押すたびに画像メモリの中身をコンソールログへ書き出すように。その結果が以下。

    https://studio-happyvalley.com/wp/wp-content/uploads/f14e1a2a04beab0b6bd99ac16e0b42b8.png初期状態では画像は2点のみ表示なので画像メモリにも2点のみ格納されているhttps://studio-happyvalley.com/wp/wp-content/uploads/010e0264b671d8fbda2bfa27c2df6e1a.png段目が2ページ目の表示。2段目が3ページ目の表示。 3段目は二度目の1ページ目。つまり1週しても画像メモリの中身は5個のままで増えていない。画像自体にはメモリリークは発生していないことが確認できた。
    しかしそれでもIMG要素の増加はとまらない

    画像のメモリリークは解消しているので、IMG要素の増加はストップしたはずと思い開発ツールで確認。
    が、変わらず、増え続けていく・・・。

    https://studio-happyvalley.com/wp/wp-content/uploads/057b65c772339ce43228a930361e3b03.png変わらず、5個を超えてIMG要素は増え続ける・・・。
    画像のメモリリークは解消したはずなのに、なぜIMG要素が増え続ける

    画像のメモリリークは解消しているのにIMG要素が増え続けるのはなぜか、もっとも論理的な結論はIMG要素のみが増え続けている!!ということ。つまりメモリタブの増え続けるIMG要素は画像ではなく、文字通りIMG要素だけなのでは!という推論。
    さっそくその推測に従いコードを変更することに。

    https://studio-happyvalley.com/wp/wp-content/uploads/fig1-20.png
    IMG要素を最初から準備しておくように変更したコード

    推論にしたがい、1ページに表示する画像枚数分IMG要素をgalleryオブジェクト内へ配置。

    IMG要素を最初から準備サンプル:https://labo.studio-happyvalley.com/garbage_collection/good/

    開発ツールの結果は

    開発ツールを見ると、見事にメモリリークを解消!

    https://studio-happyvalley.com/wp/wp-content/uploads/2b9f83dbf1ceb0c93d6cedb254ed5ec8.png

結論

メモリ管理で重要なのは、プログラムの構成であり構造である
  • ガベージコレクションは万能ではない、プログラマーのセンスが重要

    今回の研究は、まさしく晴天の霹靂!
    いつも習慣でよく考えもせずに書いていたコードがいかにメモリリークを発生させる問題点を抱えていたかということを認識できる内容でした。
    メモリ管理で重要なのは不要になったから削除するとか、表示しないからメモリを解放するとか、そういう枝葉のことではなくもっと根本、プログラムの構成や、メモリー上のデータの配置を熟考することが大切ということが理解できました。
    メモリ管理というと、これまで避けていた節があるので、これからは逃げずに取り組めそうです!

『メモリよ飛び放て!』関連のお薦め

このページで紹介しているプログラムやビジュアルなどご依頼いただければ実装を賜ります。
お問い合わせはこちら