Monday, December 22, 2025

Java 25: Compact Object Headers

Java 25 introduces Compact Object Headers, an optimisation that reduces the memory overhead of Java objects.

In my previous post, I wrote about how you can measure the size of java objects using JOL, and inspect the size of the object header. For example, take the following class:

public class Point {
  int x;
  int y;
}

Use JOL to inspect its layout:

import org.openjdk.jol.info.ClassLayout;

public class JolExample {
  public static void main(String[] args) {
    System.out.println(ClassLayout.parseClass(Point.class).toPrintable());
  }
}

The output is:

Point object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     N/A
  8   4        (object header: class)    N/A
 12   4    int Point.x                   N/A
 16   4    int Point.y                   N/A
 20   4        (object alignment gap)    
Instance size: 24 bytes

This shows that even though the Point class only has 2 int fields requiring a total of 8 bytes, the actual object uses three times that amount (24 bytes), due to the object header (12 bytes) and alignment (4 bytes).

Now let's turn on Compact Object Headers using the following JVM flag:

-XX:+UseCompactObjectHeaders

Rerunning JOL, outputs the following:

Point object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     N/A
  8   4    int Point.x                   N/A
 12   4    int Point.y                   N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

As shown above, with compact object headers enabled, the object header now takes 8 bytes instead of 12, a saving of 4 bytes.

Previously, the object header layout was split into a mark word (8 bytes) and a class word (4 bytes). With compact object headers, the division between the mark and class words is removed, and the class word is subsumed into the mark word for a total of 8 bytes.

Thursday, December 18, 2025

Measuring Java Object Size with JOL (Java Object Layout)

JOL (Java Object Layout) is a small but powerful tool developed by the OpenJDK team that lets you inspect and measure how Java objects are actually laid out in memory.

With JOL, you can:

  • Inspect object headers and field offsets
  • See padding and alignment effects
  • Measure shallow and deep object sizes
  • Compare layouts across JVM configurations

Let's start with the following simple class:

public class Point {
  int x;
  int y;
}

Now use JOL to inspect its layout using ClassLayout:

import org.openjdk.jol.info.ClassLayout;

public class JolExample {
  public static void main(String[] args) {
    System.out.println(ClassLayout.parseClass(Point.class).toPrintable());
  }
}

The output is:

Point object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     N/A
  8   4        (object header: class)    N/A
 12   4    int Point.x                   N/A
 16   4    int Point.y                   N/A
 20   4        (object alignment gap)    
Instance size: 24 bytes

This shows that even though the Point class only has 2 int fields requiring a total of 8 bytes, the actual object uses three times that amount (24 bytes), due to the object header (12 bytes) and alignment (4 bytes).

Shallow Size vs. Deep Size

The shallow size is the memory consumed by the object itself, excluding objects it references i.e. it includes the fields, object header and padding, but not referenced objects.

The deep size, on the other hand, includes the entire object graph reachable from the object.

To demonstrate this, let's look at the following example:

public class Address {
  private final String city;

  public Address(String city) {
    this.city = city;
  }
}

public class Person {
  private final String name;
  private final Address address;
  private final int age;

  public Person(String name, Address address, int age) {
    this.name = name;
    this.address = address;
    this.age = age;
  }
}

Here is the JOL output, which shows the shallow size of the Address and Person:

> ClassLayout.parseClass(Address.class).toPrintable();

Address object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     N/A
  8   4                    (object header: class)    N/A
 12   4   java.lang.String Address.city              N/A
Instance size: 16 bytes

> ClassLayout.parseClass(Person.class).toPrintable();

Person object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     N/A
  8   4                    (object header: class)    N/A
 12   4                int Person.age                N/A
 16   4   java.lang.String Person.name               N/A
 20   4            Address Person.address            N/A
Instance size: 24 bytes

As shown above, the Person's shallow size includes the object header, age and object references (name and address), but does not include the String object for name, Address object, String inside Address, or any backing char[] or byte[] arrays.

To see the deep size of the Person, use GraphLayout instead of ClassLayout, like this:

import org.openjdk.jol.info.GraphLayout;

public class JolExample {
  public static void main(String[] args) {
    final Address address = new Address("London");
    final Person person = new Person("Alice", address, 30);
    System.out.println(GraphLayout.parseInstance(person).toFootprint());        
  }
}

The output is:

Person@27abe2cdd footprint:
     COUNT       AVG       SUM   DESCRIPTION
         2        24        48   [B
         1        16        16   Address
         1        24        24   Person
         2        24        48   java.lang.String
         6                 136   (total)

That's 136 bytes in total. Note that each String is backed by a byte array (represented by [B) which is 24 bytes.

Therefore, Person is only 24 bytes shallow, but costs 136 bytes deep.

Tuesday, December 16, 2025

Running Ubuntu on Windows 11 with WSL

Windows Subsystem for Linux (WSL) makes it easy to run a full Linux environment directly on Windows 11. This is great for developers and power users who want Linux tools (e.g. grep, sed, awk, tmux) alongside Windows apps.

This is how you can install Ubuntu on Windows 11:

  1. Open PowerShell in administrator mode by right-clicking and selecting "Run as administrator", run the wsl --install command, and then restart your machine.
  2. Run wsl --list --online to list the distributions that can be installed, as shown below:
    PS > wsl --list --online
    
    The following is a list of valid distributions that can be installed.
    Install using 'wsl.exe --install <Distro>'.
    
    NAME                            FRIENDLY NAME
    AlmaLinux-8                     AlmaLinux OS 8
    AlmaLinux-9                     AlmaLinux OS 9
    AlmaLinux-Kitten-10             AlmaLinux OS Kitten 10
    AlmaLinux-10                    AlmaLinux OS 10
    Debian                          Debian GNU/Linux
    FedoraLinux-43                  Fedora Linux 43
    FedoraLinux-42                  Fedora Linux 42
    SUSE-Linux-Enterprise-15-SP7    SUSE Linux Enterprise 15 SP7
    SUSE-Linux-Enterprise-16.0      SUSE Linux Enterprise 16.0
    Ubuntu                          Ubuntu
    Ubuntu-24.04                    Ubuntu 24.04 LTS
    archlinux                       Arch Linux
    kali-linux                      Kali Linux Rolling
    openSUSE-Tumbleweed             openSUSE Tumbleweed
    openSUSE-Leap-16.0              openSUSE Leap 16.0
    Ubuntu-20.04                    Ubuntu 20.04 LTS
    Ubuntu-22.04                    Ubuntu 22.04 LTS
    OracleLinux_7_9                 Oracle Linux 7.9
    OracleLinux_8_10                Oracle Linux 8.10
    OracleLinux_9_5                 Oracle Linux 9.5
    openSUSE-Leap-15.6              openSUSE Leap 15.6
    SUSE-Linux-Enterprise-15-SP6    SUSE Linux Enterprise 15 SP6
    
  3. Run wsl --install Ubuntu (or another distro of your choice)

That’s it! Ubuntu is now running on your Windows 11 system.

You can now open a Ubuntu terminal by running the wsl command.

You can access your Windows files from /mnt/c. You can also install Ubuntu packages using apt.

Saturday, May 03, 2025

Java 24: Structured Concurrency

With Java 24, Structured Concurrency moves closer to becoming a first-class feature in the Java platform. This is currently a preview language feature.

Traditional concurrency in Java often results in fragmented and error-prone code, where related threads are launched independently and can be hard to manage or coordinate. For example, to fetch a user and order in parallel, and then process the results, you would typically use an ExecutorService as shown below:

ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> userFuture = executor.submit(() -> fetchUser());
Future<String> orderFuture = executor.submit(() -> fetchOrder());
String user = userFuture.get();   // blocks until user is fetched
String order = orderFuture.get(); // blocks until order is fetched
String result = process(user, order);

The downsides of the above approach are:

  • If one task fails, the other continues unless manually cancelled
  • The executor and tasks outlive the method unless explicitly shut down
  • You must manage the executor, handle exceptions, and ensure cleanup

Structured Concurrency abstracts much of this complexity, allowing you to focus on what your code is doing rather than how to coordinate threads. It enforces a hierarchical structure, in which tasks spawned together must complete together, much like local variables within a method.

StructuredTaskScope
Here is an example of using the StructuredTaskScope API:

try (var scope = new StructuredTaskScope<String>()) {
  Subtask<String> userTask = scope.fork(() -> fetchUser());
  Subtask<String> orderTask = scope.fork(() -> fetchOrder());

  scope.join(); // Wait for all subtasks to complete

  String user = userTask.get();
  String order = orderTask.get();

  System.out.println("user: " + user);
  System.out.println("order: " + order);
}

StructuredTaskScope has two subclasses, ShutdownOnSuccess and ShutdownOnFailure, to control how the scope reacts to task completion or failure.

StructuredTaskScope.ShutdownOnFailure
With this policy, if any task fails, the scope cancels the remaining tasks, and propagates the exception when throwIfFailed() is called.

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
  Subtask<String> userTask = scope.fork(() -> fetchUser());
  Subtask<String> orderTask = scope.fork(() -> fetchOrder());

  // wait for all subtasks to complete, or one to fail
  scope.join();
  
  // throw if any subtask failed 
  scope.throwIfFailed();

  String user = userTask.get();
  String order = orderTask.get();

  System.out.println("user: " + user);
  System.out.println("order: " + order);
}

StructuredTaskScope.ShutdownOnSuccess
This policy is the opposite — it stops once one task succeeds, cancelling the others. It's great when you want the first successful result and don't care about the rest.

try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
  scope.fork(() -> fetchFromPrimary());
  scope.fork(() -> fetchFromBackup());

  // wait for any subtask to complete, or all to fail
  scope.join();

  // get the result of the first task that completed successfully,
  // or throw an exception if none did
  System.out.println(scope.result()); 
}

Sunday, March 30, 2025

Java 24: Scoped Values

Java 24 introduces Scoped Values, a powerful alternative to ThreadLocal that offers better performance and cleaner code for managing per-thread data. This is a preview language feature.

Here's an example of ScopedValue in action:

private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();

public void handle(Request req, String userId) {
  ScopedValue.where(USER_ID, userId)
    .run(() -> handle(req));
}

private void handle(Request req) {
  String data = getData(req);
  // Do something else
}

private String getData(Request req) {
  return runQuery(req, USER_ID.get());
}

As shown above, ScopedValue provides a means to pass data (the userId) securely to a faraway method without using method parameters. The faraway method can access the data via the ScopedValue object. This eliminates the need to pass additional parameters explicitly through multiple method calls.

ScopedValue vs ThreadLocal
  • Scoped Values are immutable once set inside ScopedValue.where(...). On the other hand, ThreadLocal allows values to be changed at any time, which can lead to inconsistent state across different parts of a request.
  • Scoped Values are automatically removed after the scope ends, whereas ThreadLocal requires an explicit call to remove() to avoid memory leaks, especially in thread pools.
  • Scoped Values bind data to a specific execution scope, ensuring that when a new task starts on a thread, it doesn’t inherit values from a previous request. ThreadLocal stores data at the thread level, meaning values persist across multiple tasks when using a thread pool.
  • Scoped Values work well with virtual threads and structured concurrency APIs.

For comparison, here's the same example using ThreadLocal:

private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();

public void handle(Request req, String userId) {
  try {
    USER_ID.set(userId);
    handle(req);
  } finally {
    USER_ID.remove(); // to prevent memory leaks
  }
}

private void handle(Request req) {
  String data = getData(req);
  // Do something else
}

private String getData(Request req) {
  return runQuery(req, USER_ID.get());
}

While ThreadLocal still has its uses, most new applications will benefit from Scoped Values’ immutability, automatic cleanup, and better thread management.