Showing posts with label performance. Show all posts
Showing posts with label performance. Show all posts

Tuesday, February 16, 2021

Restrictions with record classes

In a previous post, we saw how a record class gets converted to a regular class definition during compilation. Though that conceptually shows how a record class behaves, it doesn't represent the conversion accurately. 

Lets see how exactly a record class is getting converted during compilation. We will use the same simple, single line Cat record definition for this analysis

record Cat(String color) {}

Lets compile this record class using the below command

javac --enable-preview --release 15 Cat.java

This will generate the Cat.class file in the same folder. We will analyze this class using reflection. For this purpose, we will use the below reflect utility - which prints out the basic details about the class to the console.

import java.lang.reflect.Modifier;

public class ReflectClass {

    public static void reflect(Class<?> ref) {
        log("Name",ref.getName());
        log("Module",ref.getModule().getName());
        log("PackageName",ref.getPackageName());
        log("Superclass",ref.getSuperclass().getName());
        log("Modifiers",Modifier.toString(ref.getModifiers()));
        log("DeclaredConstructors", ref.getDeclaredConstructors());
        log("DeclaredFields", ref.getDeclaredFields());
        log("DeclaredMethods", ref.getDeclaredMethods());
        
    }

    static void log(String name, Object[] values) {
        log(String.format("****%s****", name));
        int i = 0;
        for (Object val:values) {
            log(name+"-"+i++, val.toString());
        }
    }

    static void log(String name, String value) {
        log(String.format("%-25s: %s", name, value));
    }

    static void log(String data) {
        System.out.println(data);;
    }
    
    public static void main(String args[]) throws ClassNotFoundException {
        reflect(Class.forName(args[0]));
    }

}

Now lets try to analyze the Cat class using this utility by running the below command 

java --enable-preview ReflectClass Cat

This generates the output shown below

Name                     : Cat
Module                   : null
PackageName              :
Superclass               : java.lang.Record
Modifiers                : final
****DeclaredConstructors****
DeclaredConstructors-0   : Cat(java.lang.String)
****DeclaredFields****
DeclaredFields-0         : private final java.lang.String Cat.color
****DeclaredMethods****
DeclaredMethods-0        : public final boolean Cat.equals(java.lang.Object)
DeclaredMethods-1        : public final java.lang.String Cat.toString()
DeclaredMethods-2        : public final int Cat.hashCode()
DeclaredMethods-3        : public java.lang.String Cat.color()

Key details from the output highlighted in yellow. Inferences from this output are

1. The generated Cat class extends java.lang.Record. All record classes extend this class. It implies that a record class cannot extend any other class

2. Also we see the class generated is final - indicating that this record class cannot be extended by any other class

3. The record components gets converted to private final instance variables and there are not setter methods. The record components are immutable and cannot be changed after initialization. Note however that this immutability is shallow and reference attributes within a component may be mutable

4. A significant difference to note when working records is the deviation from the Java convention that the public accessor method is named color() and not getColor(). The generated accessor method does not follow the getter convention

Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Restrictions-with-record-classes


Tuesday, February 09, 2021

EpsilonGC and analyzing heap dump generated

Lets try to analyze the heap dump when our test program exits with OutOfMemoryError when run with EpsilonGC configuration. 

Code we use for this is the same test program we used in the previous post, which is 

class TestEpsilonGC {

    static final int ALLOCATION_SIZE = 1024 * 1024; // 1MB
    static final int LOOP_COUNT = 12;  
    static long start;

    public static void main(String[] args) {
        start();        
        for (int i = 0; i < LOOP_COUNT; i++) {
            System.out.print("Allocating iteration: "+i);
            byte[] array = new byte[ALLOCATION_SIZE];
            System.out.println(" Done");
        }
        end();
    }
    
    static void start() {
        start = System.currentTimeMillis();
}
    
    static void end() {
            System.out.println("Time taken: "+(System.currentTimeMillis() - start)+"ms");
    }
}

Now, lets run the below command to create the heap dump when the program exits with OutOfMemoryError

java -verbose:gc -Xmx10M -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+HeapDumpOnOutOfMemoryError TestEpsilonGC 

This creates the .hprof file in the folder under which you executed the above command. Analyzing it with IBM Heap Analyzer, we find that all objects allocated by the code are listed in the heap dump


In our case, all the byte array objects created before OutOfMemoryError got thrown is listed. 

Remember when analyzing heap dump generated with EpsilonGC configuration, objects occupying the heap space are not necessarily leaked objects. All objects ever created during the run gets listed in the heap dump.

You could use EpsilonGC to 
  • Verify the performance characteristics of your application without any interference of GC overhead
  • Benchmark the heap space to duration of run for your application before it runs out of memory
  • Study all objects getting created and validate it against the design intent of your application
  • You could even choose to use it as final configuration for your application if your application is short lived. In this case having intermittent GC cycles could be undesirable as when application terminates, it will release all the memory
  • Or this could be useful for certain batch processes which are non memory intensive, which can restart and continue where it left off from the previous run 

We could use a simple technique to restart self when OutOfMemoryError is thrown. The next post details how this can be done. 

Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Garbage-Collection/EpsilonGC/analyzing-heap-dump

Monday, February 08, 2021

Measuring performance benefits of using EpsilonGC

Lets do a quick experiment to measure the performance benefit of using EpsilonGC.

For this, lets modify the previous program a bit adding in the code for instrumentation. We will use this modified code for our experiment

class TestEpsilonGC {

    static final int ALLOCATION_SIZE = 1024 * 1024; // 1MB
    static final int LOOP_COUNT = 12;  
    static long start;

    public static void main(String[] args) {
        start();        
        for (int i = 0; i < LOOP_COUNT; i++) {
            System.out.print("Allocating iteration: "+i);
            byte[] array = new byte[ALLOCATION_SIZE];
            System.out.println(" Done");
        }
        end();
    }
    
    static void start() {
        start = System.currentTimeMillis();
}
    
    static void end() {
            System.out.println("Time taken: "+(System.currentTimeMillis() - start)+"ms");
    }
}

Lets run this code with default GC now. We will increase the max heap size to 15M so as to allow the program to complete successfully with both the configurations. 

Here is the command and the output

java -verbose:gc -Xmx15M TestEpsilonGC
[0.012s][info][gc] Using G1
Allocating iteration: 0 Done
Allocating iteration: 1 Done
Allocating iteration: 2 Done
Allocating iteration: 3 Done
Allocating iteration: 4[0.075s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 8M->0M(16M) 2.041ms
[0.075s][info][gc] GC(1) Concurrent Cycle
 Done
Allocating iteration: 5 Done
Allocating iteration: 6 Done
Allocating iteration: 7 Done
Allocating iteration: 8 Done
[0.077s][info][gc] GC(1) Pause Remark 10M->10M(16M) 0.578ms
Allocating iteration: 9[0.078s][info][gc] GC(1) Pause Cleanup 12M->12M(16M) 0.108ms
 Done
Allocating iteration: 10[0.078s][info][gc] GC(1) Concurrent Cycle 3.002ms
 Done
Allocating iteration: 11[0.081s][info][gc] GC(2) To-space exhausted
[0.081s][info][gc] GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation) 14M->0M(16M) 2.460ms
[0.081s][info][gc] GC(3) Concurrent Cycle
 Done
[0.083s][info][gc] GC(3) Pause Remark 2M->2M(14M) 0.644ms
[0.084s][info][gc] GC(3) Pause Cleanup 2M->2M(14M) 0.116ms
[0.084s][info][gc] GC(3) Concurrent Cycle 2.742ms
Time taken: 22ms

It has taken about 22ms to complete.

Now lets run this same program with EpsilonGC enabled. Command used and output are

java -verbose:gc -Xmx15M -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC TestEpsilonGC
[0.010s][info][gc] Using Epsilon
[0.010s][warning][gc,init] Consider enabling -XX:+AlwaysPreTouch to avoid memory commit hiccups
[0.049s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 814K (5.30%) used
Allocating iteration: 0[0.055s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 1919K (12.50%) used
 Done
Allocating iteration: 1[0.056s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 2943K (19.16%) used
 Done
Allocating iteration: 2[0.056s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 3967K (25.83%) used
 Done
Allocating iteration: 3[0.056s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 4991K (32.50%) used
 Done
Allocating iteration: 4[0.057s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 6015K (39.16%) used
 Done
Allocating iteration: 5[0.057s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 7039K (45.83%) used
 Done
Allocating iteration: 6[0.058s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 8063K (52.50%) used
 Done
Allocating iteration: 7[0.058s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 9087K (59.16%) used
 Done
Allocating iteration: 8[0.059s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 10111K (65.83%) used
 Done
Allocating iteration: 9[0.059s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 11135K (72.50%) used
 Done
Allocating iteration: 10[0.060s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 12159K (79.16%) used
 Done
Allocating iteration: 11[0.060s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 13183K (85.83%) used
 Done
Time taken: 13ms
[0.065s][info   ][gc     ] Heap: 15360K reserved, 15360K (100.00%) committed, 13372K (87.06%) used

This has taken just 13ms as against the 22ms taken by the G1 gc configuration. There clearly is a performance advantage in using EpsilonGC. But its use will be limited to very few special cases. 

Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Garbage-Collection/EpsilonGC/Measure-of-performance-benefits