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
| Property | PermGen (Java 7) | Metaspace (Java 8+) |
|---|---|---|
| Location | Java heap | Native memory |
| Default max | Fixed (64–256 MB) | Unlimited (bounded by OS memory) |
OutOfMemoryError | PermGen space | Metaspace (much rarer) |
| Tuning flag | -XX:MaxPermSize | -XX:MaxMetaspaceSize |
| Auto-grows? | No | Yes |
| GC’d? | With full GC | Yes (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:
- Level 0: Interpreter — runs immediately, no compilation
- Level 1–3: C1 compiler — fast compilation with basic optimisations
- 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
| Change | Impact |
|---|---|
| PermGen → Metaspace | OutOfMemoryError: PermGen space essentially eliminated; native memory grows automatically |
| Tiered compilation default | Faster startup + better peak performance out of the box |
| Type annotations | Enables pluggable null/type safety tools (Checker Framework, NullAway) |
| Repeated annotations | Cleaner API design for frameworks that apply the same annotation multiple times |
| Unsigned arithmetic | Integer.parseUnsignedInt, compareUnsigned — for binary protocol and C interop |
| String deduplication | Reduce heap for dup-heavy apps with G1GC + -XX:+UseStringDeduplication |
| G1GC production-ready | Better 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 8 → Java 11 → Java 17 → Java 21