Part 15 of 16

JVM Improvements: Metaspace, PermGen Removal, and Performance

PermGen Removal: The End of a Classic Error

Before Java 8, the JVM heap was divided into several regions. One of them — Permanent Generation (PermGen) — held class metadata, interned strings, and bytecode. PermGen had a fixed maximum size (defaulting to 64–256 MB depending on the JVM version) and could not grow beyond it.

The result: applications that loaded many classes (frameworks with heavy reflection, dynamic class generation, long-lived application servers) routinely threw:

java.lang.OutOfMemoryError: PermGen space

The standard fix was to add -XX:MaxPermSize=512m and hope for the best.

Java 8 removed PermGen entirely and replaced it with Metaspace.


Metaspace

Metaspace stores the same data as PermGen — class metadata, method bytecode, constant pool — but lives in native memory (outside the Java heap) and grows automatically.

Key Differences

PropertyPermGen (Java 7)Metaspace (Java 8+)
LocationJava heapNative memory
Default maxFixed (64–256 MB)Unlimited (bounded by OS memory)
OutOfMemoryErrorPermGen spaceMetaspace (much rarer)
Tuning flag-XX:MaxPermSize-XX:MaxMetaspaceSize
Auto-grows?NoYes
GC’d?With full GCYes (class unloading)

What This Means in Practice

OutOfMemoryError: PermGen space essentially disappears with Java 8 for most applications. Applications that hot-reload classes (Tomcat, JRebel, scripting engines) no longer need to be restarted to clear class metadata.

Metaspace JVM Flags

# Limit Metaspace to prevent native memory exhaustion
-XX:MaxMetaspaceSize=256m

# Initial Metaspace size (reduces early resizing)
-XX:MetaspaceSize=64m

# Log class unloading events (for debugging class loader leaks)
-XX:+TraceClassUnloading

# Java 11+: print Metaspace summary at shutdown
-XX:+PrintMetaspaceSummary

For most production applications, setting -XX:MaxMetaspaceSize=256m is enough to prevent unbounded native memory growth.

Class Loader Leaks

Metaspace can still leak if class loaders are not garbage collected. A class loader is reachable as long as any of its loaded classes is reachable. Common cause: storing a class instance (not just an object) in a static variable across redeploys in an application server.

# Diagnose with jmap
jmap -clstats <pid>
# or with Java Mission Control → Heap → Class histogram

Interned Strings

Before Java 7, interned strings were stored in PermGen. Java 7 moved them to the heap. Java 8’s Metaspace does not store interned strings — they remain in the heap.

String s1 = "hello";
String s2 = new String("hello");
String s3 = s2.intern(); // returns the interned copy from the string pool

System.out.println(s1 == s3); // true — same pool entry
System.out.println(s1 == s2); // false — s2 is a new object

In Java 8, the string pool is in the main heap and is GC’d normally. You can tune its size with -XX:StringTableSize=131072 (default 60013 buckets in Java 8).


Type Annotations (JEP 104)

Java 8 extended the annotation system to allow annotations anywhere a type is used — not just on declarations. This enables pluggable type-checking tools like the Checker Framework.

// Before Java 8: annotations only on declarations
@NonNull String name;

// Java 8: annotations on types in any position
@NonNull String name;                    // field declaration
List<@NonNull String> names;             // type argument
@NonNull String @Nullable [] arr;        // array component and array itself
Map<@NonNull String, @NonNull Integer> m;

// Method return type
public @NonNull String getName() { ... }

// Cast
String result = (@NonNull String) rawObject;

// new expression
String s = new @NonNull String("hello");

// throws clause
void method() throws @Critical IOException { ... }

Type annotations are consumed by tools at compile time (Checker Framework, NullAway, Error Prone) — they have no runtime overhead by default. They are the foundation for @NonNull/@Nullable null safety analysis tools.


Repeated Annotations (JEP 120)

Before Java 8, you could not use the same annotation type more than once on the same element. Java 8 added @Repeatable to solve this:

// Define the container annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedules {
    Schedule[] value();
}

// Mark the repeated annotation with @Repeatable
@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedule {
    String cron();
}

// Now you can repeat it
@Schedule(cron = "0 9 * * MON-FRI")
@Schedule(cron = "0 12 * * SAT")
public void runJob() { ... }

// At runtime, read via getAnnotationsByType
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);

Commonly used in frameworks: Spring’s @ComponentScan, Jakarta EE’s @SqlResultSetMapping.


Unsigned Integer Arithmetic (JEP 130)

Java 8 added static methods to Integer and Long for treating signed integers as unsigned values. This is useful when working with binary protocols, hash values, or C library interop:

// Integer as unsigned (values 0 to 2^32 - 1)
int a = Integer.MAX_VALUE + 1; // wraps to Integer.MIN_VALUE = -2147483648

// Parse as unsigned
int parsed = Integer.parseUnsignedInt("4294967295"); // 2^32 - 1

// To unsigned string
String s = Integer.toUnsignedString(parsed); // "4294967295"

// Unsigned comparison (treats sign bit as value bit)
int x = -1;  // 0xFFFFFFFF — as unsigned, this is 4294967295
int y = 1;
System.out.println(Integer.compareUnsigned(x, y)); // positive: x > y as unsigned

// Unsigned divide and remainder
int quotient = Integer.divideUnsigned(-1, 2);   // -1 as unsigned is 4294967295
int remainder = Integer.remainderUnsigned(-1, 3);

JVM Performance Improvements

Tiered Compilation Default

Java 7 introduced tiered compilation but didn’t enable it by default. Java 8 turned it on. Tiered compilation uses multiple JIT compilation levels:

  1. Level 0: Interpreter — runs immediately, no compilation
  2. Level 1–3: C1 compiler — fast compilation with basic optimisations
  3. Level 4: C2 compiler — slow but aggressive optimisation (used for hot code)

The result: faster startup (C1 kicks in early) AND better peak performance (C2 for hot paths).

# Tiered compilation is on by default in Java 8+
# To disable (not recommended):
-XX:-TieredCompilation

# See JIT compilation events
-XX:+PrintCompilation

G1GC Available (Default Changed in Java 9)

Java 8 ships G1GC as a production-ready collector (though CMS was still the default). You can opt in:

-XX:+UseG1GC

G1GC divides the heap into equal-sized regions and prioritises collecting the regions with the most garbage — hence “Garbage First”. It provides more predictable pause times than CMS for large heaps (>4 GB).

String Deduplication (Java 8u20+)

With G1GC, you can enable automatic string deduplication to reduce heap usage for applications with many duplicate strings:

-XX:+UseG1GC -XX:+UseStringDeduplication

The GC identifies strings with the same character content and makes them share the underlying char[]. Effective for applications with many duplicate string values (logs, user names, product codes).


Migration from Java 7: JVM-Level Concerns

Removed JVM Flags

# These flags no longer exist in Java 8 — remove from startup scripts
-XX:PermSize=256m       → use -XX:MetaspaceSize=256m
-XX:MaxPermSize=512m    → use -XX:MaxMetaspaceSize=512m

Starting Java 8 with -XX:MaxPermSize prints a warning but doesn’t fail. Remove these flags to keep JVM output clean.

GC Log Format Changed

Java 8 still uses the old GC log format. Java 9 unified GC logging; if you’re on Java 8, your existing GC log parsers (GCEasy, GCViewer) work unchanged.

-d32 / -d64 Flags Removed

The -d32 and -d64 flags (to force 32-bit or 64-bit JVM) were removed. The JVM is always 64-bit on modern hardware.


Summary

ChangeImpact
PermGen → MetaspaceOutOfMemoryError: PermGen space essentially eliminated; native memory grows automatically
Tiered compilation defaultFaster startup + better peak performance out of the box
Type annotationsEnables pluggable null/type safety tools (Checker Framework, NullAway)
Repeated annotationsCleaner API design for frameworks that apply the same annotation multiple times
Unsigned arithmeticInteger.parseUnsignedInt, compareUnsigned — for binary protocol and C interop
String deduplicationReduce heap for dup-heavy apps with G1GC + -XX:+UseStringDeduplication
G1GC production-readyBetter option than CMS for large heaps; enable with -XX:+UseG1GC

Next Step

Java 8 Best Practices and Patterns for Production Code →

Part of the DevOps Monk Java tutorial series: Java 8Java 11Java 17Java 21