How to leverage Java 10’s Thread-Local Handshakes for performance tuning

Java 10 introduced Thread-Local Handshakes as a powerful feature in the JVM, designed to improve the performance and responsiveness of applications, especially in scenarios involving thread-based operations and garbage collection.

Here’s what you need to know about Thread-Local Handshakes and how to leverage them for performance tuning:


1. What are Thread-Local Handshakes?

Thread-Local Handshakes allow a thread to execute a callback function locally without stopping all threads. This contrasts with traditional global safepoints in Java, where all threads must come to a safe state before any work can be done, such as garbage collection or code deoptimization.

In other words:

  • A handshake is a mechanism to perform operations on a subset of threads (or even individual threads) without stopping the entire JVM.
  • This is useful for operations that don’t require a global JVM safepoint, improving responsiveness and reducing latency.

2. Benefits of Thread-Local Handshakes

  • Avoids Global Safepoints: Operations can target some threads or a single thread, meaning other threads continue their work unaffected.
  • Reduces Latency: No need to pause all threads, improving performance for multithreaded applications.
  • Fine-Grained Control: Perform thread-specific tasks like flushing thread-specific memory buffers, deoptimizing code for just one thread, or collecting specific thread-local objects without interrupting the entire JVM.

3. Use Cases

Here are some scenarios where Thread-Local Handshakes can be beneficial:

  • Garbage Collection
    Garbage collectors rely on safepoints to pause threads while managing memory. Thread-Local Handshakes can isolate such operations to only the threads that need it, reducing pause times and improving application throughput.

  • Code Deoptimization
    This happens during just-in-time (JIT) compilation when compiled code needs to revert to interpreted mode. Utilizing handshakes allows deoptimization to occur on specific threads, minimizing the impact on other threads.

  • Thread-Specific Profiling and Debugging
    A developer or monitoring agent can perform profiling or diagnostic tasks on a single thread without disturbing other threads.

  • Thread-Specific Resource Cleanup
    Thread-local data structures can be cleaned up or flushed for specific threads, optimally managing system resources.


4. How Thread-Local Handshakes Work Internally

Thread-Local Handshakes introduce thread-specific “safepoints.” When a request is initiated:

  1. The JVM signals specific threads to execute a callback function (like releasing resources or processing pending tasks).
  2. Unlike global safepoints, only the targeted thread(s) pause and execute the operation.
  3. Once the operation is complete, the thread resumes execution.

This makes operations more granular and non-blocking at the JVM level.


5. Leveraging Thread-Local Handshakes in Performance Tuning

Although Thread-Local Handshakes are implemented at the JVM level, you can indirectly leverage them for performance tuning in the following ways:

  1. Tuning for Garbage Collection
    If you’re using a garbage collector like G1GC or ZGC, you can reduce garbage collection pauses since these collectors take advantage of handshakes to avoid halting all threads during certain operations.

    • How to Monitor: Use tools like Java Mission Control (JMC), VisualVM, or JVM logging to monitor GC pause times and ensure thread-local synchronization is being effectively utilized.

    Relevant JVM Options:

    • -XX:+UseG1GC (or any GC of choice) to enable advanced garbage collection strategies.
    • Use -Xlog:gc to monitor GC logs and observe pauses.
  2. Reducing Latency in Thread-Sensitive Applications
    If your application uses many threads (e.g., for handling requests or background tasks), Thread-Local Handshakes reduce overall latency by targeting specific threads instead of pausing all threads unnecessarily.

    Best practices:

    • Profile your application for thread contention and safepoints using tools like Async Profiler or JFR (Java Flight Recorder).
    • Optimize thread management through thread pools (using ForkJoinPool, ThreadPoolExecutor, etc.) to prevent thread starvation and maximize throughput.
  3. Tuning Thread-Specific Tasks
    For tasks that manipulate thread-local data or thread-specific settings:

    • Optimize performance by ensuring the work is allocated to specific threads that need operations (e.g., specific callbacks).
    • Reduce contention by designing operations that leverage locality (thread-local memory, caches, etc.).

6. Practical Tips for Developers

While Thread-Local Handshakes are managed by the JVM, the following tips help you align your code and architecture to take full advantage:

  1. Choose Modern JVMs: Use JDK 10 or later for applications where fine-grained thread optimization matters. Newer garbage collectors like ZGC or Shenandoah optimize handshakes even further.

  2. Monitor Safepoints and Utilization:

    • Safepoint statistics can be enabled using -XX:+PrintSafepointStatistics to understand how your threads interact with JVM-managed resources.
    • Use tools like JFR to detect safepoint delays or thread-local handshake activity.
  3. Minimize Global Syncs in Application Code:
    • Avoid global thread synchronization where possible.
    • Use thread-local structures (e.g., ThreadLocal API) for thread-scoped data.
  4. Benchmark Your Application:
    Profile how your code interacts with the JVM and threads. Use tools like JMH (Java Microbenchmark Harness) for thread and synchronization benchmarking.


7. Example: Monitoring Thread Safepoints

java -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+LogVMOutput -XX:LogFile=safepoints.log -jar YourApp.jar

This will output safepoint-related logs, showing where Thread-Local Handshakes may improve performance by reducing pauses.


8. Conclusion

Thread-Local Handshakes represent an evolutionary step in how the JVM manages thread interactions, replacing costly global operations with thread-targeted approaches. While you may not directly invoke or control handshakes, you can optimize your application and JVM configuration to reap their benefits:

  • Select JVM options and garbage collection strategies that leverage handshakes.
  • Profile and diagnose thread safepoints to find opportunities for performance tuning.

These adjustments ensure better efficiency, reduced latency, and improved performance in multithreaded applications.

How to Use Container-Aware JVM Features in Java 10 for Docker

Java 10 introduced new container-aware JVM features that greatly improve how Java applications run in Docker environments. These features provide enhanced automatic detection and utilization of container-based limits for memory and CPU resources, allowing Java applications to respect the constraints of containers better.

Here’s a step-by-step guide to using the container-aware JVM features in Java 10 for Docker:


1. Understand the Features

Before Java 10, the JVM didn’t recognize container resource limits (like those set by Docker). With Java 10, the JVM can now:

  • Detect container memory limits (e.g., --memory or -m in Docker).
  • Detect container CPU limits (e.g., --cpus in Docker).
  • Adjust garbage collection (GC) behavior based on allocated container resources.

2. Key JVM Options

Java 10 enables container awareness by default, but you can check and fine-tune these settings using certain JVM options:

  • -XX:MaxRAMPercentage
    Allows you to define the maximum available heap memory as a percentage of the container’s total memory limit (default: 25%).

  • -XX:InitialRAMPercentage
    Sets the initial heap size as a percentage of the container’s memory limit.

  • -XX:MinRAMPercentage
    Specifies the minimum heap size as a percentage of the container’s memory.

  • -XX:ActiveProcessorCount
    Lets you manually define the number of CPUs the JVM should consider if it doesn’t automatically detect container limits or you want to override them.


3. Check Container-Aware JVM Behavior

You can check if the JVM recognizes the container limits by running a simple Java program inside a Docker container. Below is an example:

Java Code:

public class ContainerAwarenessTest {
    public static void main(String[] args) {
        System.out.println("Available processors: " + Runtime.getRuntime().availableProcessors());
        System.out.println("Max memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB");
    }
}

4. Test in Docker

  1. Write a Dockerfile
    Create a Dockerfile using a Java 10 JDK image for testing:

    FROM openjdk:10-jdk
    COPY ContainerAwarenessTest.java /usr/src/myapp/
    WORKDIR /usr/src/myapp
    RUN javac ContainerAwarenessTest.java
    CMD ["java", "ContainerAwarenessTest"]
    
  2. Build and Run the Docker Container
    • Build the Docker image:
    docker build -t java-container-awareness .
    
    • Run the container with memory and CPU limits:
    docker run --memory="512m" --cpus="1" java-container-awareness
    
  3. Expected Output
    • The Runtime.getRuntime().maxMemory() will show 512 MB or close to it.
    • The Runtime.getRuntime().availableProcessors() will report 1 processor.

5. Fine-Tune with JVM Options

To customize the JVM’s behavior further using Java 10’s new options, add the JVM options with the java command. For example:

docker run --memory="1g" --cpus="2" java-container-awareness java \
 -XX:MaxRAMPercentage=50.0 \
 -XX:InitialRAMPercentage=25.0 \
 -XX:ActiveProcessorCount=1 \
 ContainerAwarenessTest

This manually adjusts:

  • The maximum heap to 50% of the container memory limit (1 GB).
  • The initial heap to 25% of the container memory limit.
  • The active processor count to override to only 1.

6. Verify

For detailed resource information, you can also enable verbose GC logging to monitor heap and memory usage in real-time:

docker run --memory="512m" --cpus="1" java-container-awareness java \
 -Xlog:gc \
 ContainerAwarenessTest

7. Move Beyond Java 10 [Optional]

If you’re using newer Java versions (like Java 11 or later), these container-aware features are still present, and additional enhancements have been made to how Java applications behave in containers. Make sure your base image and application are updated as needed.


By using these container-aware JVM features, your Java applications will better respect container resource constraints, leading to improved efficiency and performance in Dockerized environments.

How to Work with the Root Certificates Included in Java 10

In Java 10, root certificates are included as part of the cacerts file in the Java Runtime Environment (JRE) to establish trust for security protocols like TLS/SSL. Java includes a default set of trusted Certificate Authorities (CAs) in this file. Here’s how you can work with the root certificates in Java 10:


Accessing the Root Certificates

Root certificates in Java 10 are found in the cacerts file, which is located in the lib/security directory in your JRE or JDK installation:

  • Path for JDK: <JAVA_HOME>/lib/security/cacerts
  • Path for JRE: <JAVA_HOME>/jre/lib/security/cacerts (if the setup includes a separate JRE)

Managing Root Certificates Using the keytool Utility

Java provides the keytool command-line utility to manage keystores such as cacerts. You can use it to list, add, or remove root certificates. Here’s how:

1. List Certificates

To view the existing certificates in the cacerts keystore, use the following command:

keytool -list -keystore <JAVA_HOME>/lib/security/cacerts

By default, the password for the cacerts keystore is changeit.

2. Import a New Root Certificate

If you have a custom root certificate (e.g., mycert.crt) that needs to be trusted by Java, import it as follows:

keytool -import -trustcacerts -file mycert.crt -keystore <JAVA_HOME>/lib/security/cacerts -alias myalias
  • Replace mycert.crt with the file path of your certificate.
  • Replace myalias with a unique alias for the certificate.
  • Note: If no password change has been applied, the default password is changeit.

3. Remove a Certificate

If you need to remove a root certificate from the cacerts keystore:

keytool -delete -alias myalias -keystore <JAVA_HOME>/lib/security/cacerts

Replace myalias with the alias of the certificate you want to remove.

4. Change Keystore Password

To change the default password (changeit) for the keystore:

keytool -storepasswd -keystore <JAVA_HOME>/lib/security/cacerts

Exporting Certificates

To export a certificate from the keystore:

keytool -export -alias myalias -file mycert.crt -keystore <JAVA_HOME>/lib/security/cacerts

Troubleshooting and Tips

  1. Backup Before Modifying: Always create a backup of the cacerts file before making changes. If something goes wrong, you can restore the original file.
    cp <JAVA_HOME>/lib/security/cacerts <JAVA_HOME>/lib/security/cacerts.bak
    
  2. Certificate Format: Ensure that the certificates you are working with are in the correct format. Java usually requires certificates in PEM or DER format.

  3. Java Home Environment Variable: Ensure the JAVA_HOME environment variable is set correctly to point to your Java 10 installation.

  4. Truststore for Applications: Applications that need a specific set of certificates can use a custom keystore/truststore by specifying the following JVM arguments:

    -Djavax.net.ssl.trustStore=/path/to/custom/truststore
       -Djavax.net.ssl.trustStorePassword=yourpassword
    

Switching to a Custom Truststore

If you prefer to use a custom truststore instead of altering the cacerts file:

  1. Create a new keystore file:
    keytool -genkey -alias myalias -keyalg RSA -keystore mytruststore.jks
    
  2. Add certificates to this custom truststore using the steps outlined above.

  3. Point the application or JVM to your custom truststore using the -Djavax.net.ssl.trustStore parameter.

Conclusion

Working with the root certificates in Java 10 provides more control over establishing trust with certificate authorities. Tools like keytool simplify this management process, whether you’re adding, removing, or listing certificates in the cacerts keystore. Always follow security best practices when modifying trust settings, and ensure critical backups are in place.

How do I use new Java 10 methods like List.copyOf(), Set.copyOf(), and Map.copyOf()?

Java 10 introduced the List.copyOf(), Set.copyOf(), and Map.copyOf() methods as convenient ways to create unmodifiable copies of existing collections. These methods are part of the java.util package and provide a simpler way to create immutable collections compared to using older methods like Collections.unmodifiableList().

Here’s how you can use them:


1. List.copyOf()

The List.copyOf() method creates an unmodifiable copy of the provided Collection. The returned list:

  • Is immutable (you cannot add, remove, or modify elements).
  • Rejects null elements (throws a NullPointerException).

Example:

package org.kodejava.util;

import java.util.List;

public class ListCopyExample {
    public static void main(String[] args) {
        // Create a mutable list
        List<String> originalList = List.of("A", "B", "C");

        // Create an unmodifiable copy
        List<String> unmodifiableList = List.copyOf(originalList);

        // Print the copied list
        System.out.println(unmodifiableList);

        // Throws UnsupportedOperationException if modification is attempted
        // unmodifiableList.add("D");

        // Throws NullPointerException if original list has nulls
        // List<String> listWithNull = new ArrayList<>();
        // listWithNull.add(null);
        // List.copyOf(listWithNull);
    }
}

2. Set.copyOf()

The Set.copyOf() method creates an unmodifiable copy of the provided Collection, ensuring that:

  • The returned set contains no duplicate elements.
  • Null elements are not allowed.
  • The original collection can be a List, Set, or any Collection.

Example:

package org.kodejava.util;

import java.util.Set;

public class SetCopyExample {
   public static void main(String[] args) {
      // Create a mutable set
      Set<String> originalSet = Set.of("A", "B", "C");

      // Create an unmodifiable copy
      Set<String> unmodifiableSet = Set.copyOf(originalSet);

      // Print the copied set
      System.out.println(unmodifiableSet);

      // Throws UnsupportedOperationException
      // unmodifiableSet.add("D");
   }
}

3. Map.copyOf()

The Map.copyOf() method creates an unmodifiable copy of the provided map. Similar to List.copyOf() and Set.copyOf():

  • The returned map is immutable.
  • Null keys or values are not allowed.
  • Elements retain the original insertion order (if applicable, e.g., for LinkedHashMap).

Example:

package org.kodejava.util;

import java.util.Map;

public class MapCopyExample {
   public static void main(String[] args) {
      // Create a mutable map
      Map<Integer, String> originalMap = Map.of(1, "One", 2, "Two", 3, "Three");

      // Create an unmodifiable copy
      Map<Integer, String> unmodifiableMap = Map.copyOf(originalMap);

      // Print the copied map
      System.out.println(unmodifiableMap);

      // Throws UnsupportedOperationException
      // unmodifiableMap.put(4, "Four");
   }
}

Notes:

  1. Immutable Behavior:
    • Any attempt to modify the unmodifiable collections (e.g., using add() or put()) throws UnsupportedOperationException.
    • These methods return a new collection, but if the input collection is already immutable and meets the conditions, it may return the original collection (performance optimization).
  2. Handling Nulls:
    • If any input collection contains null elements, these methods will throw a NullPointerException.
  3. Differences from Existing Methods:
    • Unlike Collections.unmodifiableList()/Set()/Map(), these methods create a copy, ensuring that changes to the source collection won’t affect the new collection.
  4. Static Imports:
    • These methods belong to static utility classes (List, Set, and Map) and are invoked directly as static methods.

Summary:

  • Use these methods to get immutable copies of collections.
  • They reject null values by design.
  • Collections become unmodifiable and can’t be changed after creation.

They are great for enhancing immutability and safety of the application!

How to monitor memory with Java 10’s improved GC interface

Java 10 introduced enhancements to the Garbage Collection (GC) interface through the JEP 304: GC Interface, which abstracts garbage-collection implementations to improve integration and monitoring capabilities. While these improvements primarily simplify the addition of new garbage collectors to the JVM, they can also be leveraged to monitor memory usage and GC behavior in real time.

Here’s how to monitor memory using Java 10’s improved GC interface.

Key Concepts

The primary tools for monitoring memory and garbage collection (from Java 10 onward) include:
1. java.lang.management package: Interfaces and classes such as GarbageCollectorMXBean, MemoryMXBean, and MemoryPoolMXBean are still accessible.
2. java.util.logging or external libraries: For logging GC activity.
3. New Unified Logging framework: Can be used to log GC activities in detail starting with Java 9.


Steps to Monitor Memory Using Java 10 GC Interface:

1. Use the GarbageCollectorMXBean

The GarbageCollectorMXBean allows you to track details such as the number of collections, total collection time, and more.

Here’s an example:

package org.kodejava.lang.management;

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;

public class GcMonitoringDemo {
    public static void main(String[] args) {
        // Get all GC beans
        List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();

        for (GarbageCollectorMXBean gcBean : gcBeans) {
            System.out.println("Garbage Collector: " + gcBean.getName());
            System.out.println("Collection count: " + gcBean.getCollectionCount());
            System.out.println("Collection time (ms): " + gcBean.getCollectionTime());
        }

        // Simulate some memory load
        for (int i = 0; i < 10000; i++) {
            String[] temp = new String[1000];
            temp = null; // Let the memory be collected
        }

        System.out.println("After memory load:");
        for (GarbageCollectorMXBean gcBean : gcBeans) {
            System.out.println("Garbage Collector: " + gcBean.getName());
            System.out.println("Collection count: " + gcBean.getCollectionCount());
            System.out.println("Collection time (ms): " + gcBean.getCollectionTime());
        }
    }
}

Output will include:

  • Garbage collector names based on the JVM (e.g., G1 Young Generation, G1 Old Generation, etc.).
  • Collection count and total collection time.

2. Analyze Memory Usage via the MemoryMXBean

The MemoryMXBean interface helps monitor heap and non-heap memory usage.

package org.kodejava.lang.management;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;

public class MemoryMonitoringDemo {
    public static void main(String[] args) {
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();

        // Get heap memory usage
        MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
        System.out.println("Heap Memory Usage:");
        System.out.println("  Init: " + heapMemoryUsage.getInit());
        System.out.println("  Used: " + heapMemoryUsage.getUsed());
        System.out.println("  Max: " + heapMemoryUsage.getMax());
        System.out.println("  Committed: " + heapMemoryUsage.getCommitted());

        // Get non-heap memory usage
        MemoryUsage nonHeapMemoryUsage = memoryMXBean.getNonHeapMemoryUsage();
        System.out.println("Non-Heap Memory Usage:");
        System.out.println("  Init: " + nonHeapMemoryUsage.getInit());
        System.out.println("  Used: " + nonHeapMemoryUsage.getUsed());
        System.out.println("  Max: " + nonHeapMemoryUsage.getMax());
        System.out.println("  Committed: " + nonHeapMemoryUsage.getCommitted());
    }
}

3. Monitor GC Using Unified Logging

Starting from Java 9, the new Unified Logging Framework allows you to log GC activities comprehensively. You can enable it with various JVM options.

For example:

java -Xlog:gc* -XX:+UseG1GC -jar YourApplication.jar

Additional useful options include:

  • -Xlog:gc+heap: Logs GC and heap events.
  • -Xlog:gc+age: Logs information about object aging.
  • -Xlog:gc*=info,safepoint: Logs GC and safe-point information.

Output in the log will provide in-depth GC activity for analysis.


4. Advanced Real-Time Monitoring with JFR (Java Flight Recorder)

Java Flight Recorder (JFR) is another tool integrated into the JVM that enables detailed profiling and monitoring, including GC data.

java -XX:StartFlightRecording=filename=recording.jfr,duration=60s -XX:+UnlockCommercialFeatures -jar YourApplication.jar

After this recording, you can analyze recording.jfr in tools such as Java Mission Control (JMC).


5. Third-Party Tools for Active Monitoring

You can also leverage external tools or libraries:

  • VisualVM: Provides a GUI-based approach to monitor GC and memory usage.
  • micrometer.io: A metrics library for monitoring in microservices.
  • Prometheus + Grafana: To build custom dashboards for GC and memory metrics.

Conclusion

  • For basic JVM-based monitoring, use the GarbageCollectorMXBean and MemoryMXBean.
  • For detailed runtime logging of GC behavior, use the Unified Logging Framework.
  • For comprehensive profiling and diagnostics, use tools like JFR or VisualVM.

Java 10’s GC interface improvements make it easier to add and monitor new garbage collector implementations, but the existing Java Management Extensions (JMX) and logging tools are still central to effective memory monitoring.