JVM とパフォーマンスのチューニング
La Java仮想マシン(JVM) これはすべての Java アプリケーションの中心です。 内部機能と最適化テクニックを理解することで、 よりパフォーマンスの高いアプリケーションを作成し、メモリの問題を診断します。
何を学ぶか
- JVM アーキテクチャ
- メモリ領域: スタック、ヒープ、メタスペース
- ガベージコレクションとアルゴリズム
- フラグを使用した JVM チューニング
- プロファイリングと診断
- メモリリークと最適化
JVM アーキテクチャ
JVM は 3 つの主要コンポーネントで構成されます。 クラスローダー, ランタイムデータ領域 e 実行エンジン.
JVM の主要コンポーネント
| 成分 | 関数 |
|---|---|
| クラスローダー | クラスのロード、リンク、初期化 |
| ランタイムデータ領域 | スタック、ヒープ、メソッドエリア、PCレジスタ |
| 実行エンジン | インタプリタ、JITコンパイラ、ガベージコレクタ |
// Il Class Loading avviene in 3 fasi:
// 1. Loading - Legge il bytecode (.class)
// 2. Linking - Verifica, Prepara, Risolve
// 3. Initialization - Esegue static blocks e inizializzazioni
public class ClassLoadingDemo {
// Static block eseguito durante initialization
static {
System.out.println("Classe caricata e inizializzata!");
}
public static void main(String[] args) {
// Ottiene il ClassLoader corrente
ClassLoader loader = ClassLoadingDemo.class.getClassLoader();
System.out.println("ClassLoader: " + loader);
// Gerarchia dei ClassLoader:
// 1. Bootstrap ClassLoader (classi core Java)
// 2. Platform/Extension ClassLoader
// 3. Application/System ClassLoader (classpath)
// Caricamento dinamico di una classe
try {
Class<?> clazz = Class.forName("java.util.ArrayList");
System.out.println("Classe caricata: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
メモリ領域
JVM は、それぞれが特定の目的を持つ複数のメモリ領域を管理します。
ランタイムデータ領域
| エリア | コンテンツ | 共有 |
|---|---|---|
| ヒープ | オブジェクトと配列 | はい (すべてのスレッドの中で) |
| スタック | メソッドフレーム、ローカル変数 | いいえ (スレッドごとに 1 つ) |
| メタスペース | メタデータクラス、メソッド領域 | Sì |
| PCレジスタ | 現在の命令アドレス | いいえ (スレッドごとに 1 つ) |
| ネイティブメソッドスタック | ネイティブメソッドのスタック | いいえ (スレッドごとに 1 つ) |
public class MemoryAreasDemo {
// Variabile di istanza -> HEAP (dentro l'oggetto)
private int instanceVar = 10;
// Variabile statica -> METASPACE
private static String staticVar = "static";
public void metodo() {
// Variabili locali primitive -> STACK
int localPrimitive = 5;
// Reference locale -> STACK, oggetto -> HEAP
String localString = new String("hello");
// Array: reference in STACK, contenuto in HEAP
int[] array = new int[10];
}
public static void main(String[] args) {
// Ogni chiamata a metodo crea un nuovo Stack Frame
// Stack Frame contiene:
// - Variabili locali
// - Operand Stack
// - Reference al constant pool
MemoryAreasDemo demo = new MemoryAreasDemo();
// demo (reference) -> STACK
// new MemoryAreasDemo() (oggetto) -> HEAP
demo.metodo();
}
}
ヒープ構造
// L'Heap è diviso in generazioni:
// YOUNG GENERATION (Eden + Survivor Spaces)
// - Oggetti appena creati vanno qui
// - Minor GC frequenti e veloci
// - Diviso in: Eden, Survivor 0 (S0), Survivor 1 (S1)
// OLD GENERATION (Tenured)
// - Oggetti sopravvissuti a più GC cycles
// - Major GC meno frequenti ma più lenti
// - Oggetti grandi possono andare direttamente qui
// Flag per dimensionare l'heap:
// -Xms512m Heap iniziale (512 MB)
// -Xmx2g Heap massimo (2 GB)
// -Xmn256m Young Generation (256 MB)
// Esempio di configurazione:
// java -Xms1g -Xmx4g -Xmn512m MyApplication
// Visualizzare memoria a runtime:
public class HeapDemo {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory(); // -Xmx
long totalMemory = runtime.totalMemory(); // Heap corrente
long freeMemory = runtime.freeMemory(); // Libera nel corrente
long usedMemory = totalMemory - freeMemory;
System.out.println("Max Memory: " + maxMemory / (1024 * 1024) + " MB");
System.out.println("Total Memory: " + totalMemory / (1024 * 1024) + " MB");
System.out.println("Free Memory: " + freeMemory / (1024 * 1024) + " MB");
System.out.println("Used Memory: " + usedMemory / (1024 * 1024) + " MB");
}
}
ガベージコレクション
Il ガベージコレクター (GC) メモリを自動的に解放します もう到達できないオブジェクトによって占有されています。
public class GCDemo {
public static void main(String[] args) {
// 1. Nullifying reference
Object obj1 = new Object();
obj1 = null; // Ora elegibile per GC
// 2. Reassigning reference
Object obj2 = new Object(); // Primo oggetto
obj2 = new Object(); // Primo oggetto ora elegibile
// 3. Objects inside methods
creaOggetti(); // Oggetti creati nel metodo elegibili dopo return
// 4. Island of isolation
class Nodo {
Nodo next;
}
Nodo a = new Nodo();
Nodo b = new Nodo();
a.next = b;
b.next = a;
a = null;
b = null;
// Entrambi elegibili (nessun reference esterno)
// Suggerire GC (non garantito!)
System.gc();
Runtime.getRuntime().gc();
}
static void creaOggetti() {
Object local = new Object();
// local elegibile per GC dopo che il metodo termina
}
}
ガベージコレクションアルゴリズム
ガベージコレクターが利用可能
| GC | フラグ | 特徴 |
|---|---|---|
| シリアル GC | -XX:+UseSerialGC | シングルスレッド、小規模アプリケーション向け |
| 並列 GC | -XX:+UseParallelGC | マルチスレッド、スループットの最適化 |
| G1 GC | -XX:+G1GC を使用する | デフォルトの Java 9+、短い一時停止、大きなヒープ |
| ZGC | -XX:+ZGC を使用する | 超低停止 (10 ミリ秒未満)、巨大なヒープ |
| シェナンドー | -XX:+ShenandoahGC を使用する | 短い一時停止、同時圧縮 |
// Flag comuni per GC tuning:
// Scegliere il GC
// -XX:+UseG1GC (default Java 9+)
// -XX:+UseZGC (pause ultra-basse)
// -XX:+UseParallelGC (throughput)
// G1 GC tuning
// -XX:MaxGCPauseMillis=200 Target pausa max (default 200ms)
// -XX:G1HeapRegionSize=4m Dimensione regioni (1-32 MB)
// -XX:InitiatingHeapOccupancyPercent=45 Soglia concurrent GC
// GC Logging (Java 9+)
// -Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10m
// Esempio completo:
// java -Xms2g -Xmx4g \
// -XX:+UseG1GC \
// -XX:MaxGCPauseMillis=100 \
// -Xlog:gc*:file=gc.log \
// MyApplication
// Statistiche GC a runtime:
import java.lang.management.*;
import java.util.List;
public class GCStatsDemo {
public static void main(String[] args) {
List<GarbageCollectorMXBean> gcBeans =
ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gc : gcBeans) {
System.out.println("GC Name: " + gc.getName());
System.out.println("Collection Count: " + gc.getCollectionCount());
System.out.println("Collection Time: " + gc.getCollectionTime() + " ms");
System.out.println();
}
}
}
JITコンパイル
Il ジャストインタイム (JIT) コンパイラー バイトコードをネイティブコードにコンパイルします 実行時に、コードの最も実行される部分を最適化します。
// La JVM usa Tiered Compilation (default):
// Tier 0: Interprete puro
// Tier 1: C1 compiler (compilazione veloce, poche ottimizzazioni)
// Tier 2: C1 con profiling limitato
// Tier 3: C1 con profiling completo
// Tier 4: C2 compiler (ottimizzazione massima)
// Flag JIT:
// -XX:+TieredCompilation (default on)
// -XX:TieredStopAtLevel=1 Ferma a C1 (startup veloce)
// -XX:-TieredCompilation Solo C2 (server apps)
// -XX:CompileThreshold=10000 Soglia per compilazione
// Vedere cosa viene compilato:
// -XX:+PrintCompilation
// Esempio output:
// 123 1 3 java.lang.String::hashCode (55 bytes)
// 125 2 4 java.util.HashMap::hash (20 bytes)
// Colonne: timestamp, id, tier, metodo, dimensione bytecode
// Verificare ottimizzazioni:
public class JitDemo {
public static void main(String[] args) {
// Loop caldo - sarà compilato da JIT
long sum = 0;
for (int i = 0; i < 100_000; i++) {
sum += calcola(i);
}
System.out.println("Sum: " + sum);
}
// Questo metodo verrà inlined dal JIT
static int calcola(int n) {
return n * 2 + 1;
}
}
プロファイリングと診断
// 1. jconsole / jvisualvm - GUI monitoring
// Avviare con: jconsole oppure jvisualvm
// 2. jps - Lista processi Java
// $ jps -l
// 12345 com.example.MyApp
// 3. jstat - Statistiche GC
// $ jstat -gc <pid> 1000 (ogni 1000ms)
// $ jstat -gcutil <pid> 1000
// 4. jmap - Memory dump
// $ jmap -histo <pid> Lista oggetti
// $ jmap -dump:file=heap.hprof <pid> Heap dump
// 5. jstack - Thread dump
// $ jstack <pid>
// 6. Java Flight Recorder (JFR)
// $ java -XX:StartFlightRecording=duration=60s,filename=recording.jfr MyApp
// Analisi programmatica memoria:
import java.lang.management.*;
public class MemoryProfilingDemo {
public static void main(String[] args) {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
// Heap memory
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
System.out.println("Heap:");
System.out.println(" Init: " + heapUsage.getInit() / (1024*1024) + " MB");
System.out.println(" Used: " + heapUsage.getUsed() / (1024*1024) + " MB");
System.out.println(" Committed: " + heapUsage.getCommitted() / (1024*1024) + " MB");
System.out.println(" Max: " + heapUsage.getMax() / (1024*1024) + " MB");
// Non-heap (Metaspace)
MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
System.out.println("Non-Heap:");
System.out.println(" Used: " + nonHeapUsage.getUsed() / (1024*1024) + " MB");
// Thread info
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
System.out.println("Thread count: " + threadBean.getThreadCount());
System.out.println("Peak thread count: " + threadBean.getPeakThreadCount());
}
}
メモリリークと最適化
import java.util.*;
public class MemoryLeakExamples {
// 1. LEAK: Static collections che crescono
private static List<byte[]> cache = new ArrayList<>();
public void addToCache(byte[] data) {
cache.add(data); // Mai rimosso!
}
// FIX: Usare cache con limite
// private static LRUCache cache = new LRUCache(100);
// 2. LEAK: Listener non rimossi
class Button {
private List<ActionListener> listeners = new ArrayList<>();
void addListener(ActionListener l) {
listeners.add(l);
}
// Manca removeListener()!
}
interface ActionListener { void onClick(); }
// 3. LEAK: Inner class mantiene reference all'outer
class Outer {
byte[] bigData = new byte[10_000_000];
class Inner {
// Mantiene implicitamente reference a Outer!
}
Inner createInner() {
return new Inner();
}
}
// FIX: Usare static inner class
// static class Inner { ... }
// 4. LEAK: Connessioni/risorse non chiuse
public void readFile() {
try {
// FileInputStream fis = new FileInputStream("file.txt");
// Leggi...
// Manca fis.close()!
} catch (Exception e) {
e.printStackTrace();
}
}
// FIX: try-with-resources
public void readFileSafe() {
// try (FileInputStream fis = new FileInputStream("file.txt")) {
// // Leggi...
// } // Chiuso automaticamente
}
// 5. LEAK: HashMap con chiavi mutabili
public void hashMapLeak() {
Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>();
key.add("a");
map.put(key, "valore");
key.add("b"); // Modifica il hashCode!
// Ora map.get(key) non trova più l'entry
// L'entry rimane "orfana" nella mappa
}
}
最適化手法
import java.util.*;
public class PerformanceOptimizations {
// 1. StringBuilder per concatenazioni in loop
public String buildStringBad(int n) {
String result = "";
for (int i = 0; i < n; i++) {
result += i; // Crea nuovo String ogni volta!
}
return result;
}
public String buildStringGood(int n) {
StringBuilder sb = new StringBuilder(n * 4); // Pre-alloca
for (int i = 0; i < n; i++) {
sb.append(i);
}
return sb.toString();
}
// 2. Inizializzare collezioni con capacità
public void collectionCapacity() {
// BAD: resize multipli
List<String> list = new ArrayList<>();
// GOOD: pre-allocata
List<String> listGood = new ArrayList<>(1000);
// Stesso per HashMap (considera load factor 0.75)
Map<String, Integer> map = new HashMap<>((int)(1000 / 0.75) + 1);
}
// 3. Evitare boxing/unboxing in loop
public long sumBad(List<Integer> numbers) {
long sum = 0;
for (Integer n : numbers) {
sum += n; // Unboxing ad ogni iterazione
}
return sum;
}
public long sumGood(int[] numbers) {
long sum = 0;
for (int n : numbers) {
sum += n; // Nessun boxing
}
return sum;
}
// 4. Lazy initialization
private volatile ExpensiveObject expensive;
public ExpensiveObject getExpensive() {
if (expensive == null) {
synchronized (this) {
if (expensive == null) {
expensive = new ExpensiveObject();
}
}
}
return expensive;
}
class ExpensiveObject {}
// 5. Object pooling per oggetti pesanti
// Esempio: connection pool per database
}
一般的な JVM フラグ
構成フラグ
| フラグ | 説明 |
|---|---|
| -Xms、-Xmx | 初期および最大ヒープ |
| -Xss | スレッドスタックサイズ (例: -Xss512k) |
| -XX:メタスペースサイズ | 初期メタスペース |
| -XX:+HeapDumpOnOutOfMemoryエラー | OOM への自動ダンプ |
| -XX:ヒープダンプパス | ヒープダンプのパス |
| -XX:+PrintFlagsFinal | すべてのフラグを表示 |
# Esempio configurazione per applicazione web in produzione:
java \
-Xms4g \
-Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m \
-XX:+ExitOnOutOfMemoryError \
-jar myapp.jar
# Flag per container (Docker/Kubernetes):
java \
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-jar myapp.jar
ベストプラクティス
最適なパフォーマンスのためのルール
- 最適化する前に測定する: プロファイラーを使用する
- ヒープのサイズを正しく設定する:多すぎず少なすぎず
- 適切な GC を選択する: G1 一般用途用
- GCの監視: ロギングを有効にする
- メモリリークを避ける: リソースを閉じ、リスナーを削除します
- プリミティブを優先する: 不必要なボクシングを避ける
- コレクションの事前割り当て: 特定の初期容量
- try-with-resources を使用する: リソースを管理するため







