Works by

Ren's blog

@rennnosuke_rk 技術ブログです

JSON Web Token(JWT)

JSON Web Tokenとは

JSON Web Token(JWT)は、署名付きJSONトークンです。ネットワーク上でやり取りされるJSON形式の文字列に署名を付加することで、JSONの改ざん検出を行うことができます。

署名

JSON文字列に対して署名を施す場合、どのように署名がなされるのでしょうか。

JSONの発行者をAさんとします。Aさんは自分だけが持つ暗号化用のキー秘密鍵JSONを暗号化しました。Aさんが発行したJSON(暗号化済み)の参照者は、JSON発行者が公開した復号用のキーである公開鍵を使って、暗号化されたJSONを復号します。

公開鍵は、対となるたったひとつの秘密鍵で暗号化された文字列を復号可能です。公開鍵による復号が上手く行った時、その文字列が対になる秘密鍵で暗号化されたことが分かります。秘密鍵を持っているのはもちろんAさんです。つまり「公開鍵で復号できる」という事実が「JSONを発行した人はAさん」であることを証明できます。

また、秘密鍵で暗号化した文字列が1ビットでも変化してしまえば、公開鍵による復号化は不可能になります。この特性から、署名によって「文字列の改ざん検出」も可能になります。

ちなみに、JSONを暗号化すると言いましたが、この行為はデータの難読化としては意味を成しません。というのも、すでに復号用の鍵は世間一般に公開されており、誰でも復号できるからです。署名による暗号・復号では改ざん検出しか行うことはできません。

また、秘密鍵が何者かに盗まれてしまえば、署名は効力を失います。悪意ある第三者がAさんになりすますことができ、任意の文字列にAさんとして署名できてしまいます。

JWTの構造

JWTは3つの構成要素からなります。

ヘッダ

データタイプや署名用のアルゴリズム等の情報を含みます。

{
  "alg": "HS256",
  "typ": "JWT"
}

ペイロード

実際にやり取りするJSON情報です。

{
  "name": "rennnosuke",
  "message": "Hello, JWT!"
}

署名

署名情報が入っています。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
) secret base64 encoded
エンコード結果

JWT.ioで好きなJSONをJWTにエンコードできます!
実際に上記JSONエンコードしてみました。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoicmVubm5vc3VrZSIsIm1lc3NhZ2UiOiJIZWxsbywgSldUISJ9.Q3ww2YF0QBKlmVHHyV07XYl3URlDSgJBTr8r83O5ddI

参考文献

JSON Web Tokens - jwt.io

【JavaScript】JavaScriptにおける等価性比較

等価性比較

ある値やオブジェクトを比較する時、各々が何を持って等しいとするかは使用する言語体系や比較演算子、関数、メソッドによって変わります。JavaScriptでも比較のための演算子として=====が用意され、またECMS2015では比較用のObjectプロパティObject.isが追加されました。これらの等価性比較の手段は、厳密には異なる動作を示します。

==演算子

==演算子による等価性比較では、各オブジェクトを共通の型に変換した上で比較を行います。

console.log(1 == 1); // true

console.log(1 == '1'); //true
console.log(1 == '-1'); //false

console.log(1 == true); //true
console.log(1 == false); //false

console.log(null == undefined) // true

console.log(NaN == NaN); // false

console.log(Number.isNaN(NaN)); // true

上記の例では、左辺のnumber型変数に対して右辺の値がnumber型に変換され、その上で比較が行われています。このように、==演算子では型の互換を許可したゆるい等価性比較が行われます。なおこの比較ではnullundefinedは等価なものとして扱われます。また、NaNはどの値に対しても等価とはなりません(NaNは他のいかなる数値とも等しくないという特性を持ちます)。NaNであるかどうかの比較にはNumber.isNaNを用います。

オペランドの型別変換対応表

f:id:rennnosukesann:20180401221011p:plain MDNより引用)

===演算子

===による比較はより厳格であり、型の暗黙的変換が行われません。すなわち、各オペランドの型が異なる時点で比較式はfalseを返します。当然、型が同じ場合でも値が異なればfalseを返します。なお、こちらの演算子でもNaN同士の比較ではfalseになります。

console.log(1 === 1); // true

console.log(1 === '1'); //false
console.log(1 === '-1'); //false

console.log(1 === true); //false
console.log(1 === false); //false

console.log(null === undefined); //false

console.log(NaN === NaN); // false

Object.is

Object.is===と同じく型変換を行わない厳密な等価比較を行いますが、以下の挙動が===とは異なっています。

  • -0と+0を区別し、両者を等しくないものとする。
  • NaN同士の比較をtrueとする。
console.log(Object.is(0, 0)); // true
console.log(Object.is(0, '0')); // false
console.log(Object.is(0, false)); // false
console.log(Object.is(0, null)); // false
console.log(Object.is(0, undefined)); //false

console.log(Object.is(0, +0)); //true
console.log(Object.is(0, -0)); // false
console.log(Object.is(NaN, NaN)); //true

Object.isを使うことで、通常の== ===演算とは異なるルールを導入してしまうので、特別なことがなければ===を使用したほうが無難です。

【Java】CMS

CMS

CMS(Concurrent Mark & Sweep)コレクタは、Javaで用いられるガーベジコレクタです。従来の世代別GC方式を採用していますが、Old領域のGC処理をコンカレントGCと呼ばれる手法によって行います。

従来の世代別GC

従来の世代別GCでは、ヒープをNew領域とOld領域に分け、New領域をさらにEden領域、Seveive(From/To)領域に分割します。新しく生成したオブジェクトはEden領域に格納され、New領域に対しては頻繁にMark&CopyによるGCが適用されます(コピー先はServive領域)。Servive領域内である一定回数GCの対象とならなかったオブジェクトはOld領域に移動し、以降はMixedGC、FullGCでのみGC対象となります。

Old領域に対するGCにはMark&Sweepが用いられます。Mark&Sweepは非GC時のパフォーマンスが良いので不要になりにくいオブジェクトが集まるOld領域にはうってつけなのですが、GC時のパフォーマンスはMark&CopyGCを下回ります。そのため、Old領域のGC=実行時間コストが大きいのです。

仮にネットワーク処理やIOであれば別スレッドの並列実行が可能ですが、GCはそもそもアプリケーションが使うメモリ上のオブジェクトデータを操作してしまうので、並列実行による裏方処理ができません。

そこで、Old領域のGC時間を最小にするコンカレントGCが登場しました。

コンカレントGC

コンカレントGCではGC処理を以下の4つフェーズに分割し、アプリケーションの停止時間が少なくなるように各フェーズを実行します。アプリケーションスレッドの動作に影響がない処理であれば、別スレッドで並列実行します。

  • Initial Mark
  • Concurrent Mark
  • Remark
  • Concurrent Sweep

Initial Mark

Mark & Sweep同様、被参照オブジェクトに印をつけるフェーズです。この処理は、全てのアプリケーションスレッドが停止する必要のある動作です。

Concurrent Mark

アプリケーションスレッドの処理と並列させながら動作するマーク作業。アプリケーションを動かしつつマークするので、例えばマークされているオブジェクトが参照されなくなっている、マークが付いていないオブジェクトだが参照されている等の矛盾が発生してしまいます。

Renark

Concurrent Markフェーズのマークの整合性をチェックします。この作業はアプリケーションと同期する必要があります。

Concurrent Sweep

マークの付いていないオブジェクトをSweepします。既に使われていないオブジェクトの破棄作業なので、アプリケーションスレッドと並列に動作できます。

f:id:rennnosukesann:20180401001116p:plain:w600

Mark&Sweepの動作を4フェーズに分けたことで、内2フェーズを並列処理で実行することができています。

CMSの推奨環境

  • マシンが複数プロセッサを搭載している
  • ヒープに格納されるオブジェクト量が多く、比較的短命である
  • アプリケーション内でGCによる一時停止時間を短くしたい

参考文献

コンカレント・マーク・スイープ(CMS)コレクタ

“Stop the World”を防ぐコンカレントGCとは?:現場から学ぶWebアプリ開発のトラブルハック(2) - @IT

【Java】G1GC

Javaのガーベジコレクタ

【Java】JVMのヒープメモリ管理の記事では、JVMがどのようなガーベジコレクタアルゴリズムを搭載しているかについて調査・紹介しました。今回はJavaGCアルゴリズムの詳細実装であるG1GC(Garbage First Garbage Collector)について調べてみました。

G1GCとは

G1GCはJava7より実装され、Java9では標準実装となったCGアルゴリズムです。 G1GCの特徴として

  • 大容量メモリ搭載マシンを対象とする
  • マルチプロセッサマシンを対象とする
  • ユーザが指定する、GCによる一時停止目標時間を高い確率で満たすことができる
  • ヒープ圧縮効率が従来のGCCMS)よりも比較的良い
  • グローバルマーキング(後述)等のGC処理はアプリケーションスレッドと並行して動作する

点が挙げられます。

G1GCのアルゴリズム

G1GCは、ヒープ領域を「リージョン」と呼ばれる均等領域に分割します。G1GCアルゴリズムは各リージョンに対して、使われていないオブジェクトに対して印をつけていきます(グローバル・マーキング・フェーズと呼ばれます)。印を付け終わったあと、印のついていないオブジェクト、すなわちガーベジでいっぱいになったリージョンが見つかった場合、G1GCは印の付いたオブジェクトだけを他の単一のリージョンにコピーし、コピー元のリージョンをクリーンします(コピーGC)。ガーベジでいっぱいとなったリージョンを優先的に開放するところが、ガーベジ・ファーストと呼ばれる所以だそうです。

リージョン間のコピーGCによって、メモリーの解放処理だけでなく圧縮にも成功しています。これは、同じくJavaのガーベジコレクタアルゴリズムであるCMS(Concullent Mark&Sweep)が圧縮処理を行わないことに対する大きなアドバンテージです。

世代別GC

【Java】JVMのヒープメモリ管理の記事では、世代別GCについて扱いました。実は、先程説明したG1GCのアルゴリズムには、世代別GCの概念が導入されています。G1GCではヒープ領域をリージョンという単位に分割すると述べましたが、このリージョンは「New領域」「Tenured領域」の二種類に分かれます。またNew領域は「Eden領域」と「Serviver領域」に分割されます。

新しく生成されたオブジェクトはEden領域に格納されていきます。Eden領域には頻繁にマーク&コピーGCが実施され、生き残ったオブジェクトは一部のServiver領域へとコピーされます(Eden領域は開放されます)。Serviver領域に対してもEdenと同じタイミングでGCが行われ、生き残ったオブジェクトは別のServiver領域へコピーされます。数回生き残ったオブジェクトはTenured領域へとコピーされ、頻繁なGCの対象とならなくなります。

f:id:rennnosukesann:20180331131333p:plain

ガベージファースト・ガベージ・コレクタ より引用 )

上図では、New領域が単色水色、Tenured領域がグラデーションの水色で示されています(なぜこの配色なのかは謎です。。)。矩形内の「S」はSuvivor領域であることを示し、赤いアイコンはGC対象領域であることを示しており、全てのNew領域と、一部のTenured領域がまさにGCされようとしています。このように毎回ではないものの、場合によってTenured領域を含むGCが走ることがあります。New領域に対するGCをYongGC、New領域と一部のTenured領域に対するGCをMixGC、JVMの対象とする、ヒープ全体へのGCをFullGCと言います。

なお、矩形内に「H」と書かれた領域はHumongous領域と呼び、大きいサイズのオブジェクトを格納するための領域です。この領域を解放するためには、FullGCを行うしかありません。

一時停止予測モデル

G1GCでは、ユーザが指定したGCによるアプリケーションの一時停止時間目標(GCによってどれくらいアプリケーションを止めてもよいか)を満たすべく、一時停止予測モデルを採用しその予測に基づいてリージョン数を調整しているそうです。

予測モデルをどのような仕組みで生成しているのかは不明ですが、モデルから将来の大まかな停止時間を導出し、ユーザが指定した停止時間目標内に収まるようリージョン数を増減させるものと思われます。なおこのユーザの目標停止時間は、JVMのフラグMaxGCPauseMillisで指定できます。

G1GCの推奨環境

ガーベジコレクションの対象となるアプリケーションが以下の条件を備えている場合、G1GCを採用するメリットが有ると公式では述べています。

  • Javaのヒープデータのうち、50%が非参照オブジェクトで占領される。

  • オブジェクトの割当率・昇格率が大きく変化する。

  • ガーベジコレクションによるアプリケーションの停止時間が望ましくない(0.5~1秒を超える)。

まとめ

G1GCでも世代別GCの概念を導入し、Mark&SweepとコピーGCによってメモリの開放と圧縮を効率的に行っていることがわかりました。次回はCMSについても詳しく調べてみようと思います。

参考文献

ガベージファースト・ガベージ・コレクタ

【Java】Javaの日時/日付に関するAPI(Java8)

Java8による時刻API

前回の記事では、Java7以前で用いられていた日時APIについて紹介しました。しかし、組み込みのAPIとしてはバグの温床となるような仕様が多く、個人的に使いづらさを感じていました。実際、

一般的な開発者が扱うような日付/時間のユースケースJavaで十分にサポートされていないことは、Java開発者にとって長年の悩みの種でした。

と現在のJava提供元であるOracleですらそう述べています*1

Java8では以下のような新しい日時のためのAPIが新しく整備され、より厳密な日時操作が可能となっています。

  • Instant
  • LocalDate
  • LocalTime
  • LocalDateTime
  • ZonedDateTime
  • ZoneId
  • OffsetTime
  • ZoneOffset
  • Period
  • Duration
  • ChronoLocalDate
  • ChronoLocalDateTime
  • ChronoZonedDateTime

【Java】Javaの日時/日付に関するAPI(Java7以前)

システムと時刻

地球上に存在する多くのシステムやアプリケーションは、「時刻」という概念に依存します。例えばRDBテーブル上のレコードが更新日時を持つ、WebアプリケーションクライアントがHTTPリクエストを投げた時刻サーバに伝えるなど、例を上げればきりがありません。時刻に関する処理はプログラムの中で記述される機会が極めて高く、時刻を扱うAPIについて熟知しておくことは大きな武器になると考えられます。今回はシステム開発で頻繁に用いられるJavaの日時APIに焦点を絞り、その特徴を調査しつつ纏めたいと思います。

Java7までの日時API

Java7までは、主に以下三つのクラスが日付を扱える代表的なAPIでした。

  • Date
  • Calendar
  • SimpleDateFormat
  • TimeZone

Java8が既に出ている今日では、これらのクラスが提供する機能をTime APIでほぼ互換可能であるため、積極的に使うべきではないという位置づけになっています。それでなくとも、各クラスの実装は幾つかの問題をはらんでいるため、優先的に使用すべきではありません。ただし、既に使われているサーバミドルウェアフレームワーク等がJava7(最悪の場合、それよりもっと古いバージョン)を利用しなければならない場合は、これらのクラスを細心の注意を払いながら使っていくことになります。

Date

Dateは長い間使われているにも関わらず、多くの問題を持つ時刻管理クラスの一つです。Dateのオブジェクトは日付情報をミリ秒精度のUNIXTIMEとして保持します。

// オブジェクトの生成(現在時刻)
Date date = new Date(); // Wed Mar 28 13:23:00 GMT 2018

// 年(!!getYearは非推奨!!)
int year = date.getYear(); // 118
        
// 月(!!getMonthは非推奨!!)
int month = date.getMonth(); // 2 

// 日(!!getDayは非推奨!!)
int day = date.getDay(); // 28

Dateオブジェクトは、標準出力によって日付表記となります。 上記の出力だけみても、

  • getYear()の返戻値が1900からの経過年数として出力される
  • getMonth()の返戻値が表す月数は0開始

という粗悪な特徴を持つことが見て取れます。これらのget関数は今では非推奨となっていますが、getMonthに関してはDateのWrapperクラスであるCalendarですら同じ挙動を示します。

またDateはタイムゾーン情報を持ちません。Date.toStringによって文字列の日時情報を参照した場合、表示される時間はシステムのタイムゾーンに依存します。getDateなどの日付情報を取得するメソッドも同様です。

加えて、Date単体ではメソッドを介しての日時の加減算は不可能であり(set系統も非推奨であるため)、後述するCalendarクラスを使用する必要があります。にもかかわらず、オブジェクトフィールドを直接書き換えることができますが、通常は避けるべきです。

個人的な意見ですが、Date型の運用する際は一貫してUNIXTIME情報を保持させることを徹底すべきです。タイムゾーン情報を持たないため、Date単体での年月日などの情報の更新・取得を行うと時差の問題が発生する可能性があります。日時の更新・取得には後述のCalendarを使うか、Java8であればtime API中のクラスの利用を検討したほうが良いでしょう。

Calendar

CalendarはDateのWrapperクラスであり、Dateの持つ幾つかの問題を解消するのに役立ちます。

// Calendarオブジェクト
Calendar calendar = Calendar.getInstance();

// タイムゾーン指定
calendar.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));

// set
// 年
calendar.set(Calendar.YEAR, 2017);
// 月(0開始)
calendar.set(Calendar.MONTH, 10);
// 日
calendar.set(Calendar.DATE, 12);
// 時(24h)
calendar.set(Calendar.HOUR_OF_DAY, 16);
// 分
calendar.set(Calendar.MINUTE, 0);
// 秒
calendar.set(Calendar.SECOND, 0);

// まとめて指定
calendar.set(2017, 10, 12, 16, 0, 0);

// get
int year = calendar.get(Calendar.YEAR); // 2017
int month = calendar.get(Calendar.MONTH); // 10
int day = calendar.get(Calendar.DATE); // 12
int hour = calendar.get(Calendar.HOUR_OF_DAY); // 16
int minute = calendar.get(Calendar.MINUTE); // 0
int second = calendar.get(Calendar.SECOND); // 0

// Calendar -> Date
Date date = calendar.getTime(); // Sun Nov 12 07:00:00 GMT 2017

// add 
calendar.add(Calendar.YEAR, 1);
calendar.add(Calendar.MONTH, 1);
calendar.add(Calendar.DATE, 1);
calendar.add(Calendar.HOUR_OF_DAY, 1);
calendar.add(Calendar.MINUTE, 1);
calendar.add(Calendar.SECOND, 1);

// Calendar -> Date
date = calendar.getTime(); // Thu Dec 13 08:01:01 GMT 2018

// Date -> Calendar
Date now = new Date(); // Wed Mar 28 15:00:01 GMT 2018
calendar.setTime(now);

Dateほどではありませんが、日付APIとしての使いづらさが目立ちます。

  • 相変わらずgetMonthで取得できる月数は0開始
  • getsetaddなどの日付操作の殆どにフラグ指定を要する
  • 日付情報をオブジェクト生成時に設定できないため、毎回setterを呼ぶ必要がある

Calendarで日付を管理する場合、可能であればタイムゾーンを明示的に指定するのが望ましいです。タイムゾーンをシステムの稼働環境に合わせたい場合もTimeZone.getDefault()を渡しておくと丁寧で良いかもしれません。

SimpleDateFormat

SimpleDateFormatは、日付フォーマットのパース・フォーマッティングをタイムゾーンを考慮しながら行います。例えば2018-02-03という文字列を日本時間としてDateに変換したり、逆にDateの持つUNIXTIMEからyyyy/MM/ddのフォーマットに即したGMT時刻文字列を生成することもできます。

// 現在時刻
Date date = new Date(); // Wed Mar 28 15:37:48 GMT 2018

// SimpleDateFormatオブジェクト
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// タイムゾーン
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
// Date -> 文字列
String formattedDate = sdf.format(date.getTime()); // 2018-03-29 00:37:48

String dateStr = "2018-03-03 10:10:00";
// 文字列 -> Date
Date parsedDate = sdf.parse(dateStr); // Sat Mar 03 01:10:00 GMT 2018

文字列とUNIXTIMEの変換では、特に注意を払う必要があります。タイムゾーンの指定は必ず行うようにしましょう。文字列からDateを生成するときも、Dateから文字列を生成するときも、SimpleDateFormatではTimeZone クラスを用いてタイムゾーンを指定できます。

タイムゾーンを指定しないとどうなるのでしょうか。日本時間の2018-03-29T00:30:00(ISO8601形式ですが、あえて時差を除いています)を表すミリ秒精度のUNIXTIME1522251000000は、タイムゾーンGMTとして解釈すると2018-03-28T13:30:00となってしまいます。逆に、日本時間の2018-03-29T00:30:00GMT2018-03-29T00:30:00は文字列としては同じですが、UNIXTIME上では1522251000000(日本)と1522218600000GMT)のように全く異なります。このように、タイムゾーンはいわば時刻変換におけるプロトコルとして機能しているので、変換時には明示的に指定すべきです。

Java7までの日時APIのなかでは比較的良心的なこのクラスですが、そもそも文字列形式の時刻表記とUNIXTIMEを相互に変換する行為自体に落とし穴があるため、バグの温床となりやすいです。Java8(TimeAPI)においてもこの問題に対しては発生しうるので、気をつけたいところです。

TimeZone

Calendar・SimpleDateFormat仕様におけるタイムゾーン指定子として重要な役割を持ちますが、それ以上に特筆する項目は無いと思われます。Java8ではこれに変わるZoneId、そしてZoneOffsetクラスが存在し、timeAPIではタイムゾーン指定子としてこれらを使うことになります。

まとめ

今回はJava7以前における日時に関するAPIについてまとめました。結構個人の思うところが露出してしまっている気もしますが、「Jaav8環境ではTimeAPIを優先して使う」「Java7以前の環境ならば十分注意して使用すべきである」点は概ね正しいと思われます。Java8 time APIについても纏めてみようと思います。

参考文献

Date (Java Platform SE 8)

Calendar (Java Platform SE 7)

java.time (Java Platform SE 8)

【Java】JVMのヒープメモリ管理

JVMとヒープ

オブジェクト指向言語として有名なJavaプログラムがコンパイルされて中間言語としてJVM上で動く時、多くのオブジェクトの生成と破棄が繰り返され、それに伴ってヒープ領域の確保や生成が行われます。そのような激しいメモリアロケーションにも関わらず、今日のシステムの多くはJavaによって構成されています。今回はJVM上で行われるヒープメモリ上の管理、特にガーベジコレクタについて調べたので、紹介します。

ガーベジコレクタ

JavaScript実行モデルの記事でも触れたとおり、ガーベジコレクタはオブジェクトを扱う高級言語にとって現在なくてはならないプログラムです。ガーベジコレクション(以下GC)によって不要なオブジェクトの開放作業を自動的に行い、プログラマが煩雑なメモリ管理・開放作業に悩まされることなく、透過的にオブジェクトを使用することを助けてくれます。

上記記事では基本的な二種類のGCの方法について紹介しました。

  • Reference Pointer
  • Mark & Sweep

Reference Pointerはオブジェクトへの参照数をカウントし、その値が0になるオブジェクトを破棄の対象とするのでした。またMark & Sweepはrootオブジェクトから参照可能なオブジェクトを辿り、参照不可能なオブジェクトを破棄する方法を採っていました。Mark & Sweepは循環参照問題を回避し、非コレクション時はReference Pointerよりもパフォーマンスに優れる特徴を持ちます。一方で、Mark & Sweep はコレクション時のコストが大きいという欠点を持ち、積極的な「ゴミ集め」をしてしまうとアプリのパフォーマンスを大きく下げる原因となってしまいます。

また、メモリの確保と開放の繰り返しによって、ヒープは小さな空き領域が点在する穴だらけの状態になってしまいます。これでは大きなひとかたまりのオブジェクトが入る隙間がなくなってしまうので、デフラグメンテーション(以下デフラグ)を行いヒープを整頓する必要がありますが、デフラグ自体にもパフォーマンスコストを要します。

コピーGC

コピーGCフラグメンテーションの問題を解決します。このアルゴリズムでは同じ要領のメモリ領域を二つ用意するのですが、ヒープ領域として用いるのは片方の領域Aだけです。Aに対してGCを実施する場合、必要なオブジェクトだけを別の領域Bへ整列させながらコピーし、Aの領域をすべてクリアすることでGCを実現します。このアルゴリズムでは、GCデフラグを同時に行えるという利点がありますが、前述の通り用意したヒープ領域の半分しか使えなくなるというデメリットがあります。

一般に、この手法は他のアルゴリズムと組み合わせて使用します。例えばMark & Sweepと組み合わせて使う場合、SweepのタイミングでコピーGCによるデフラグの恩恵をうけることができます(Mark & Copyと呼ばれるそうです)。

世代別GC

Mark & Sweep アルゴリズムGCとして採用するのであれば、コレクションを実施するタイミングについて慎重にならなければなりません。

世代別GCは、コレクションすべきオブジェクトを分別してそのタイミングを調整してくれる、より賢いアルゴリズムです。「多くのGCにさらされながらも生き残っているオブジェクトは、以降も参照される率が高い」というコンセプトに基づいて、オブジェクトの管理を行います。

このアルゴリズムでは、ヒープ領域を第一世代第二世代の二つの領域に分割します。

f:id:rennnosukesann:20180328002551p:plain:w400

第一世代では、高速なコピーGC(後述)を頻繁に行っていきます。こちらの領域は小さく、一時オブジェクトは即座に回収されます。

f:id:rennnosukesann:20180328002946p:plain:w400

第二世代では基本的にGCが行われず、システム全体でメモリ不足が発生して初めてMark & SweepによってGCが行われます。

f:id:rennnosukesann:20180328003241p:plain:w400

最後に重要なルールですが、第一世代で一定回数のGCを生き残ったオブジェクトは、第二世代に移動するという決まりがあります。

f:id:rennnosukesann:20180328003923p:plain:w400

この仕組によって、一時オブジェクトのようなすぐに使われなくなるオブジェクトは第一世代ですぐに破棄され、暫く参照されているオブジェクトは第二世代で生き延びます。第二世代ではMark & SweepによるGCが実施されますが、頻度は少ないです。

世代別GCはバランスに優れたGCとして認知されており、多くのオブジェクト指向でサポートされています。Javaのヒープ領域の管理にも、世代別GCが採用されています。

JVMにおけるGC

現在のJava(10がリリースになりました!)では厳密にこの通りかどうか不明ですが、Javaも典型的な世代別GCの仕組みを採用しています。

JVMの世代別GC

ヒープはNew領域とOld領域に別れ、それぞれが第一世代と第二世代の領域に該当します。New領域はさらに「Eden」「From」「To」の三領域に分割されます。一方でJVMにロードされたクラス情報を格納する領域も用意されているようなのですが、ここでは割愛します(Pernament領域と呼ばれるそうです)。

f:id:rennnosukesann:20180328013552p:plain:w500

新しいオブジェクトが生成された時、そのオブジェクトはEden領域に格納されます。Eden領域がいっぱいになると、To領域へのコピーGCが走ります。このとき、コピーに成功した被参照オブジェクトはデフラグされた状態でTo領域に格納されます。このタイミングでEden領域はクリアされますが、不要なオブジェクトのみが残っている状態なので問題ありません。

f:id:rennnosukesann:20180328014313p:plain:w500

f:id:rennnosukesann:20180328014458p:plain:w500

To領域にオブジェクトが存在する状態で再びEden領域がいっぱいになると、同じようにTo領域へのコピーGCが行われます。このとき、元々To領域に存在したデータはどうなるのでしょうか。実は、To領域とFrom領域がこのタイミングで入れ替わり、入れ替わったあとのFrom領域とEden領域からTo領域へとコピーが開始されます。この繰り返しにより、To領域にはGCの対象外となったオブジェクトが蓄積されていきます。

f:id:rennnosukesann:20180328015120p:plain:w500

OLD領域への移動

To領域に蓄積されていくオブジェクトは、いずれOLD領域へ移さなければいけません。そのため、To領域に存在するオブジェクトには、コピーGCによってTo領域に移動した回数が割り当てられています。回数が閾値を上回るオブジェクトが発生した場合、OLD領域への移動が開始されます。

まとめ

簡単にですが、GC手法のおさらいと新しい手法、そしてJVMにおけるメモリ管理の概要について紹介しました。C/C++のような言語でなければメモリアロケーションは普段意識しにくいところですが、例えばサーバミドルウェアの管理などでメモリ障害が起きたときなどに対応できるよう、基本的な動作だけでも理解しておきたいところです。

参考文献

参照カウント - Wikipedia

マーク・アンド・スイープ - Wikipedia

世代別ガベージコレクション - Wikipedia

Javaのヒープ・メモリ管理の仕組み:Javaパフォーマンスチューニング(3) - @IT