2026年2月11日水曜日

【Java進化史 第10回】Java8 〜あのPermGenはどこへ消えた〜

【Java進化史 第10回】Java8 〜あのPermGenはどこへ消えた〜

昔、意味も分からず書いていたオプションがある。


-XX:MaxPermSize=256m

足りなければ増やす。
512m。
1024m。

それでも、死ぬ。


java.lang.OutOfMemoryError: PermGen space

あの文字列を、何度見ただろう。

■ PermGenとは何だったのか

Java7まで、JVMのメモリは大きくこう分かれていた。


Heap
 ├─ Young領域
 ├─ Old領域
 └─ PermGen(Permanent Generation)

PermGenはヒープの一部で、主に

  • クラスのメタ情報
  • メソッド情報
  • 静的変数
  • (Java6までは)文字列定数プール

を保持していた。

そして最大の問題は、 サイズが固定だったことだ。

ヒープのように自動で伸びない。
だから溢れる。

■ Stringを多用するな、と言われた時代

あの頃、よく言われた。

「Stringを多用するな」

理由は曖昧だった。
だが確かにサーバはよく死んだ。

これは半分本当だ。

Java6までは、文字列定数プールがPermGenにあった。

大量の文字列リテラル、internの多用、
フレームワークが内部で生成する文字列。

それらがPermGenを圧迫することがあった。

だから、

「Stringがヤバい」

という空気は、間違いではなかった。

■ でも本質はStringそのものではない

問題は設計だった。

固定サイズのPermGenに、 クラス情報と文字列プールを押し込んでいたこと。

さらに、Stringは不変(immutable)。


String s = "";
for (int i = 0; i < 10000; i++) {
    s += i;
}

このコードは、大量の中間Stringを生む。

だから我々は学んだ。

「StringBuilderを使え」

間違ってはいない。
だが根本原因はJVM側の設計にもあった。

■ GCパラメータ職人の時代

最後は、GCと戦っていた。

レスポンスが急に悪くなる。
CPUが跳ね上がる。
サーバが固まる。

Full GC。

Stop The World。

ログを吐かせる。


-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:NewRatio=2
-XX:SurvivorRatio=8

意味を完全に理解していたか?

正直、怪しい。

だが、体で覚えていた。

パラメータを調整し、 再起動し、 負荷試験をし、 延命する。

今思えば、 あれは完全に職人技だった。

■ Java7で起きた静かな改善

Java7で文字列プールはヒープへ移動した。

PermGenに直接効くString問題は緩和された。

だが、PermGenそのものはまだ存在していた。

■ Java8でPermGenは消えた

Java8でPermGenは廃止された。

代わりに導入されたのが Metaspace

  • ヒープ外(ネイティブメモリ)を使用
  • 原則、動的に拡張

固定枠で突然死ぬ設計ではなくなった。

さらにGCも成熟した。

「あれ?最近安定してない?」

それは気のせいではない。

  • PermGen廃止
  • Metaspaceの動的拡張
  • GCアルゴリズムの成熟

足元が、ちゃんと進化していた。

■ あの頃の自分へ

-XX:MaxPermSizeを書き、
Stringを疑い、
GCログを読み、
サーバの負荷に怯えていた自分へ。

PermGenは、もういない。

Java8は文法革命だけではない。

JVMの土台も、ちゃんと進化していた。

少しだけ、戦わなくてよくなった。

ほんと、助かるよ。





【Java進化史 第9回】Java7 〜nullと戦ってきた世代へ〜

【Java進化史 第9回】Java7 〜nullと戦ってきた世代へ〜

昔、nullと戦っていた。

NullPointerException。
あの文字を見るだけで、少し気分が重くなる。

だから、書いた。
ひたすら書いた。


if (obj == null) {
    throw new IllegalArgumentException("obj is null");
}

引数の数だけ書く。
メソッドの数だけ書く。

equalsを書くときも同じだった。


(a == b) || (a != null && a.equals(b))

毎回これを書く。
コピペする。
少し修正する。

死ぬほどNullチェックを書いた。

でも、Nullぽが出るんだ。

あれだけ守ったつもりなのに。
あれだけ防御したつもりなのに。

■ それはボイラープレートだった

こういう「毎回ほぼ同じ形で書かされる定型コード」を、 ボイラープレート(boilerplate)という。

本質的な業務ロジックではない。
だが、安全のために、仕様のために、必ず書かされるコード。

nullチェックもそう。
equalsの安全比較もそう。

本当に書きたいのは業務ロジックなのに、
周辺の儀式コードがどんどん増えていく。

可読性は下がる。
コピペミスも起きる。
そして本質が埋もれる。

だから思った。

「これ、言語や標準API側で何とかならないのか?」

■ Commonsを入れる時代

多くの現場で入れた。

  • Jakarta Commons Lang
  • Apache Commons Lang

ObjectUtils.equals。
Validate.notNull。
HashCodeBuilder。

便利だった。
本当に助かった。

だが同時に、こうも思っていた。

「これ、最初からJDKにあってもよくないか?」

■ Java7で静かに入ったもの

Java7で java.util.Objects クラスが登場する。


Objects.equals(a, b);
Objects.requireNonNull(obj);
Objects.hash(f1, f2, f3);

たったこれだけだ。

派手ではない。
ラムダもない。
文法革命でもない。

だが、あの頃の自分には刺さった。

■ null安全な比較


Objects.equals(a, b);

もう


(a == b) || (a != null && a.equals(b))

を書かなくていい。

条件式の読み間違いも減る。
equalsの呼び出し元がnullでも落ちない。

コードが静かになる。

■ 引数チェックも一行で


this.name = Objects.requireNonNull(name);

if文を書かなくていい。

「nullなら例外」
その意図が一行で伝わる。

しかも例外は明示的。
どこで壊れたかがすぐ分かる。

■ hashCodeも簡潔に


@Override
public int hashCode() {
    return Objects.hash(id, name, age);
}

昔は31を掛けた。
あるいはHashCodeBuilderを使った。

それが標準で書ける。

■ Commonsはいらなくなったのか?

全部ではない。

StringUtilsもある。
CollectionUtilsもある。
高度なユーティリティはまだ強い。

だが少なくとも、

  • null安全比較
  • nullチェック
  • hashCode生成

このあたりは標準で足りる。

毎回Commonsを入れなくてもいい。
社内共通ライブラリを作らなくてもいい。

JDKだけで書ける。

■ これは思想の変化

Javaは昔、最小限のライブラリだった。

足りないものは外部で補う。

だがJava7は少し違った。

多くの現場で繰り返されている安全処理は、標準に取り込む。

それはつまり、

ボイラープレートを減らす方向への進化

だった。

■ あの頃の自分に言いたい

死ぬほどnullチェックを書いていた自分へ。

それでもNullぽが出て、夜にログを追いかけていた自分へ。

少しだけ、楽になる。

Java7は派手ではない。

だが、

地味だけど、確実に設計や実装の質を上げる進化をしている。

こういう進化、好きなんだよなぁ。




【Java進化史 第8回】Java7 〜例外を“正確に投げ直せる”ようになった日〜

【Java進化史 第8回】Java7 〜例外を“正確に投げ直せる”ようになった日〜

昔、本当に例外処理ばかり書いていた。

正常処理より catch の方が長い。
そんなクラスも珍しくなかった。

とにかく捕まえる。
とにかく投げる。
とにかく throws を増やす。

そして気がつけば、メソッドの宣言はこうなる。


public void process() throws Exception

もう何でもありだ。

「結局、何が飛んでくるの?」

呼び出し側はこう書く。


try {
    process();
} catch (Exception e) {
    // で、何が来るの?
}

IOException?
SQLException?
それとも別の何か?

結局、process() を読む。
さらにその中で呼ばれているメソッドも読む。

これをカプセル化と呼べるのか。

メソッドのシグネチャを見れば契約が分かる。
それがオブジェクト指向の約束ではなかったのか。

だが現実は違った。

throws Exception は便利だった。

便利だが、それは「設計の曖昧さ」を広げていただけだった。

■ Java6までの“濁り”

例えば、こんなコード。


public void process() throws Exception {
    try {
        readFile();   // throws IOException
    } catch (Exception e) {
        throw e;
    }
}

readFile() は IOException しか投げない。

だが catch (Exception e) と書いた瞬間、
コンパイラはこう判断する。

「Exception が投げられる可能性がある」

だから throws Exception になる。

正確ではない。
広がっている。

そして、その広がりは呼び出し側へ伝染する。

設計が、少しずつ濁っていく。

■ Java7の“地味な革命”

Java7で導入されたのが、
precise rethrow(例外の再スローの型推論) だ。

同じコードでもこう書ける。


public void process() throws IOException {
    try {
        readFile();   // throws IOException
    } catch (Exception e) {
        throw e;
    }
}

一見、何も変わっていない。

だがコンパイラが変わった。

Java7はこう考える。

「tryブロックの中で、実際に投げられる例外型は何か?」

readFile() が投げるのは IOException だけ。

だから再スローされる型も IOException だけだと判断する。

その結果、


throws IOException

で済む。

例外が“正確になる”。

■ 何が嬉しいのか

  • throws が広がらない
  • APIが引き締まる
  • 呼び出し側が迷わない
  • シグネチャが契約として機能する

呼び出し側はこう書ける。


try {
    process();
} catch (IOException e) {
    // 何が来るか分かっている
}

読まなくていい。
追いかけなくていい。

契約が戻ってくる。

これは小さいが、本質的な改善だ。

■ それでも万能ではない

catch 変数を再代入すると、この推論は効かない。


catch (Exception e) {
    e = new Exception();  // 再代入すると従来通り広がる
    throw e;
}

コンパイラが追跡できる範囲でのみ働く。

魔法ではない。
だが思想は明確だ。

「正確に宣言せよ」

■ 例外はどこで処理するのか

  • 発生箇所 → 技術的例外をそのまま投げる
  • 中間層 → 無理に握りつぶさない
  • 最上位 → ログ、通知、終了判断

Java6時代、広がった throws Exception は、その責任を曖昧にした。

Java7のprecise rethrowは、例外を“適切な型で上に渡す”ことを助ける。

それはつまり、責任の所在を明確にすることだ。

■ あの頃の自分に言いたい

Java7の再スローは地味だ。

だが、例外に振り回されてきた世代にとっては、 設計を少しだけ取り戻すための機能だった。

派手な進化ではない。
だが、確かに“進化”だった。

地味だけど、確実に設計や実装の質を上げる「例外の再スローの型推論」。
こういう機能、好きだなぁ。

【Java進化史 第7回】Java7 〜マルチキャッチでcatch地獄脱出〜


Java6までしか知らない自分にとって、 例外処理は「とにかくcatchを書くもの」だった。

同じ処理でも、例外ごとに延々とcatchを書く。 あの頃、どれだけcatchを書いただろう。


■ Java6以前の世界 〜catch地獄〜


try {
    process();
} catch (IOException e) {
    logger.error(e);
} catch (SQLException e) {
    logger.error(e);
} catch (ParseException e) {
    logger.error(e);
}

全部同じ処理なのに、分けて書くしかなかった。

コピペして、微妙に修正して、また増える。 気づけばcatchだらけ。

例外が増えるたびに、catchも増える。 設計ではなく、文法に縛られていた。


■ Java7で登場したマルチキャッチ


try {
    process();
} catch (IOException | SQLException | ParseException e) {
    logger.error(e);
}

一行で済む。

たったそれだけの違い。 しかし、これは確実に「進化」だった。

例外を「どう扱うか」でまとめられるようになったからだ。


■ 例外クラス自体は変わらない


public class BusinessException extends Exception {
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
}

例外を作る方法は変わらない。




■ Java7がもたらした本当の進化

マルチキャッチは単なる文法改善ではない。

「同じ扱いをする例外は、まとめられる」

つまり、

例外は“型”ではなく、“扱い”で考える。

Java7で、ようやく catch地獄から脱出できた。

50代、Javaを学び直して思う。

【Java進化史 第6回】Java7 - 数字は読めるようになったのか ― Java7数値リテラルと浮動小数点の現実

【Java進化史 第6回】数字は読めるようになったのか ― Java7数値リテラルと浮動小数点の現実

Java6までしか知らない50代が、Java7以降をやり直すシリーズ。

今回はJava7で改善された数値リテラルと、
その裏にある浮動小数点の現実について整理する。


■ 昔、数字は読めなかった

昔、数値は「正しければいい」と思っていた。
だが、あとからコードを読むとこうなる。

「これ、何桁だ?」


double amount = 1000000.123456;
double rate   = 0.00001234;
  • 桁が読みにくい
  • 金額か割合か直感で分からない
  • コメントが必要になる

ビットマスクも同じだった。


int mask = 8;   // 1000?

毎回、頭の中で2進数に変換していた。


■ Java7で何が変わったのか

1. アンダースコアが使えるようになった


double amount = 1_000_000.123_456;
double rate   = 0.000_012_34;

桁が視覚的に分かる。
コメントがなくても意味が伝わる。

2. 2進数リテラル(0b)が使えるようになった


int mask = 0b1000;
int flag = 0b0101;

ビットの意味が直接読める。

Java7は、大きな変更だけでなく、
読む人のための改善を入れてきた。


■ しかし ― 浮動小数点の罠

数字が読みやすくなっても、
計算が正確になったわけではない。

0.1 + 0.2 問題


System.out.println(0.1 + 0.2);

出力:


0.30000000000000004

浮動小数点は2進数で近似表現される。
そのため、正確な10進小数にはならない。


■ 浮動小数点どうしの比較は危険

危険な比較


if (a == b) {
    // 同じ?
}

閾値(ε)を使う


double EPS = 1e-10;

if (Math.abs(a - b) < EPS) {
    // ほぼ同じとみなす
}

実務ではこの考え方が必須になる。


■ 正確な計算が必要なら BigDecimal


BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal result = a.add(b);

System.out.println(result); // 0.3

ポイントは文字列で生成すること

やってはいけない例


BigDecimal x = new BigDecimal(0.1);
System.out.println(x);

出力:


0.1000000000000000055511151231257827021181583404541015625

doubleの誤差をそのまま引き継いでしまう。

安全な書き方


BigDecimal x = new BigDecimal("0.1");
// または
BigDecimal y = BigDecimal.valueOf(0.1);

■ 昔、私はこう信じていた

Javaを習い始めたころ、
「小数点があるなら float や double だろう」と思っていた。

そして見事にハマった。

  • 計算はズレる
  • 比較は一致しない
  • 表示すると変な桁が出る

そのとき初めて知った。

浮動小数点は“正確な小数”ではない。


■ まとめ

項目 float/double BigDecimal
精度 近似値 正確
2進数表現 あり なし(10進ベース)
比較 誤差考慮が必要 equalsで比較可能
金融用途 不向き 推奨

Java7は数字を読みやすくした。
だが、数字の本質は変わらない。

「数字が読める」ことと
「数字が正しい」ことは別問題である。




【Java進化史 第5回】Java7 - NIO.2 ― Fileで戦った俺たちは、なぜ疲れていたのか

【Java進化史 第5回】NIO.2 ― Fileで戦った俺たちは、なぜ疲れていたのか

Java6までしか知らない50代が、Java7以降をやり直すシリーズ。

今回は Java7で導入された NIO.2(java.nio.file) を扱う。


■ あの頃、ファイル操作は「共通処理」だった

昔、ファイル操作はすべて共通処理にしていた。
ディレクトリ再帰、コピー、削除、存在チェック。
全部、自分たちで書いた。
テストも自分たちでやった。

例外処理も、ログも、バッファサイズも、全部。

正直、たいへんだった。

でも当時は、それが「実力」だと思っていた。


■ Java6以前 ― Fileクラスの世界


▼ 再帰削除(Java6)


public static void deleteDirectory(File dir) {
    if (dir.isDirectory()) {
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                deleteDirectory(file);
            }
        }
    }
    dir.delete();
}
  • listFiles() が null を返す
  • delete() は boolean を返すだけ
  • 失敗理由がわからない
  • 再帰は自分で書く

だから「共通処理」にした。
そうしないと怖かった。

▼ ファイルコピー(Java6)


public static void copy(File src, File dest) throws IOException {
    InputStream in = new FileInputStream(src);
    OutputStream out = new FileOutputStream(dest);
    byte[] buffer = new byte[1024];
    int length;
    while ((length = in.read(buffer)) > 0) {
        out.write(buffer, 0, length);
    }
    in.close();
    out.close();
}
  • close漏れの恐怖
  • 例外時の後処理
  • バッファサイズ設計
  • テストケースの山

「File操作ユーティリティ」は、どの現場にもあったはずだ。


■ そしてJava7 ― NIO.2の登場

Java7で導入されたのが java.nio.file パッケージ
通称 NIO.2。

▼ Pathという思想


Path path = Paths.get("sample.txt");

Fileはクラス。
Pathはインターフェース。

ここに設計思想の違いがある。


▼ コピー(Java7)


Files.copy(
    Paths.get("a.txt"),
    Paths.get("b.txt"),
    StandardCopyOption.REPLACE_EXISTING
);

終わり。
共通処理、いらない。


▼ 再帰削除(Java7)


Files.walk(Paths.get("targetDir"))
     .sorted(Comparator.reverseOrder())
     .forEach(p -> {
         try {
             Files.delete(p);
         } catch (IOException e) {
             e.printStackTrace();
         }
     });
  • 再帰を意識しない
  • APIが責任を持つ
  • 例外が明確

■ NIO.2の「2」の意味

NIO(Java1.4)は Non-blocking IO。
だが NIO.2 の本質は非同期ではない。

File APIの再設計 である。

Fileクラスは拡張性が低く、例外設計も弱く、 シンボリックリンク対応も不十分だった。

だから、作り直した。


■ 思ったこと

あの頃、俺たちはファイル操作を「共通化」していた。

それは技術力の証だと思っていた。

でも違った。

足りなかったのは俺たちじゃない。
足りなかったのはAPIだった。


■ まとめ

項目 Java6以前 Java7以降
再帰処理 自分で実装 Files.walk
コピー ストリーム手書き Files.copy
削除 boolean戻り値 IOException
設計思想 クラス中心 インターフェース中心

NIO.2は単なる便利機能ではない。
設計思想の転換点だった。




2026年2月8日日曜日

【Java進化史 第4回】Java7 - switch文と設計思想 ― 分岐の向こう側にあるもの

【Java進化史 第4回】switch文と設計思想 ― 分岐の向こう側にあるもの

そういえば、最近あまりswitch文を書いていない。

書けなくなったわけではない。 むしろ、書こうと思えばすぐ書ける。

だが設計を考え始めると、 どこかで立ち止まる自分がいる。


■ 手続き的思考の安心感

switchは安心だ。

条件を並べれば、 処理の流れは一目で追える。

どこで何が起きるのか、 すべてが自分の視界の中にある。

長く手続き的なコードを書いてきた身には、 この感覚は心地よい。

分岐を書く。 処理を書く。 上から順に読む。

世界が、整然と並んでいる。


■ ふと浮かぶ違和感

だがあるとき、ふと思った。

オブジェクト指向言語で、 switchをあまり見ないのはなぜだろう。

書けるのに、 あまり使われない。

それは構文の問題ではない。 設計思想の違いだ。


■ 責務という考え方

オブジェクト指向では、 振る舞いはクラスに持たせる。

条件で分けるのではなく、 責務で分ける。

「どの種類か」で分岐するのではなく、 「誰が責任を持つのか」を考える。

その結果、 分岐はオブジェクトの内部に吸収される。

外側から見えるのは、 ただの振る舞いだけになる。


■ 変更点の局所化

switchを書き始めると、 同じ条件分岐が別の場所にも現れることがある。

仕様変更が入るたびに、 その条件を探して回る。

変更点が、散らばる。

一方で、責務を適切に分けた設計では、 変更は一か所に閉じ込められる。

変更点の局所化。

それは地味だが、 長く保守する現場では大きな意味を持つ。


■ 拡張に開き、修正に閉じる

新しい種類が増えたとき、 switchはcaseを増やす。

既存のコードを修正する。

だが理想とされる設計は、 拡張に開き、修正に閉じる。

既存部分に手を入れず、 新しい要素を追加することで対応する。

それがOCP(Open/Closed Principle)と呼ばれる考え方だ。

理屈は理解している。

だが、実践は簡単ではない。


■ 設計の重さ

責務を分ける。

将来の変更を想像する。

インターフェースを定義する。

switch一つで済む場面でも、 設計は少し重くなる。

正直に言えば、 急いでいるときほど、 switchの安心感に戻りたくなる。

条件を書けば、すぐに動く。

目の前の問題は、すぐ解決する。


■ 未来の自分への負債

だが安易な分岐は、 未来の自分への負債になることがある。

仕様が増え、 条件が増え、 分岐が増え、 修正箇所が増える。

そのとき向き合うのは、 過去の自分の判断だ。

設計とは、 未来の自分との対話なのかもしれない。


■ 分岐の向こう側

switchを書かなくなったのは、 構文の問題ではない。

分岐よりも、 責務を考える時間が増えただけだ。

それが成長なのか、 ただ慎重になっただけなのかはわからない。

だが少なくとも、 設計という重みを意識するようにはなった。

分岐の向こう側にあるもの。

それを考えることが、 オブジェクト指向という思想なのだと思う。





【Java進化史 第10回】Java8 〜あのPermGenはどこへ消えた〜

【Java進化史 第10回】Java8 〜あのPermGenはどこへ消えた〜 昔、意味も分からず書いていたオプションがある。 -XX:MaxPermSize=256m 足りなければ増やす。 512m。 1024m。 それでも、死ぬ。 java.l...