How to cause a java.lang.ClassCircularityError

Marcio Endo
June 27, 2022

edit 2022-08-07: rework introduction, add Javadoc links to class creation methods and add many missing words

I recently learned about the existence of the ClassCircularityError Java class. Its documentation states:

Thrown when the Java Virtual Machine detects a circularity in the superclass hierarchy of a class being loaded.

Is it bad not knowing about a Java class that:

I think it is OK to not know about it. As we will see in this blog post:

I thought learning about it would be an interesting blog post. In other words, how can we cause a ClassCircularityError?

And I believe it is great timing. The Classfile API JEP draft has just been announced. So let's get started.

What is the ClassCircularityError?

So what is the ClassCircularityError anyway?

If it were possible to compile the following Java file:

class Circular extends Circular {}

The first time a program references it, for example in a expression statement such as:

new Circular();

The JVM running such program would throw a ClassCircularityError.

Fortunately the Java compiler javac does not allow classes like Circular to be compiled.

javac does not create invalid class files

So it is clear, let's try to compile our Circular example:

$ echo "class Circular extends Circular {}" > Circular.java
$ javac Circular.java
Circular.java:1: error: cyclic inheritance involving Circular
class Circular extends Circular {}
^
1 error

As we can see, javac does not compile such class. It does not allow the following hierarchy either:

class A extends B {}

class B extends A {}

Class A would be a subclass of itself via class B. Class B would also be a subclass of itself via class A. It is a cyclic inheritance as per the previous javac error message.

As mentioned, javac does not compile it either:

$ echo "class A extends B {}" > AB.java
$ echo "class B extends A {}" >> AB.java
$ javac AB.java
AB.java:1: error: cyclic inheritance involving A
class A extends B {}
^
1 error

If javac does not compile our invalid source files, how can we cause a ClassCircularityError to be thrown?

Manually editing our class files

So if javac refuses to create an invalid class file we will have to create one ourselves.

In its goals, the Classfile API JEP draft states:

Eventually, a wide range of applications and frameworks should be to effectively use this library as an alternative to ASM, cglib, or other bytecode library.

Meaning we could use one of the mentioned libraries to create our class files.

But we will not. After all this experiment is but an excuse to learn a little about the class file format.

However creating a class file from scratch would be too much for a blog post. Therefore, we will take class files generated by javac and manually edit them instead.

Our class hierarchy

Let's define a class hierarchy containing three classes. Each class is defined in its own Java file:

package cce;

public class ClsCircErr01 {
  public ClsCircErr01() {
    System.out.println("Hello from #01");
  }
}

public class ClsCircErr02 extends ClsCircErr01 {
  public ClsCircErr02() {
    System.out.println("Hello from #02");
  }
}

public class ClsCircErr03 extends ClsCircErr02 {
  public ClsCircErr03() {
    System.out.println("Hello from #03");
  }
}

The ClsCircErr name is short for ClassCircularityError. The need for such strange name will become clear later on.

We can see that class #03 extends class #02 which, in turn, extends class #01. Each prints a message in its constructor. If we were to create a ClsCircErr03 object we would expect it to print:

Hello from #01
Hello from #02
Hello from #03

Let's manually create the Java files and compile them. In other words, we will have these source files separate from our main project. We do not want these source files in our project as they might interfere with our experiment:

$ vi ClsCircErr01.java
$ vi ClsCircErr02.java
$ vi ClsCircErr03.java
$ javac *.java
$ ls -1 *.class
ClsCircErr01.class
ClsCircErr02.class
ClsCircErr03.class

Great!

How to create a class definition at runtime?

Before we go and edit our class files, first we need to make sure that we can:

The javadocs for the Class class states:

Class has no public constructor. Instead a Class object is constructed automatically by the Java Virtual Machine when a class is derived from the bytes of a class file through the invocation of one of the following methods:

All of the mentioned methods declares a byte array parameter. Declaring large byte array literals in Java it is not so pleasant. So let's get a hex dump of all of our class files instead.

The xxd tool

We can use the xxd tool in Linux to get hex dumps:

$ xxd -plain ClsCircErr01.class
cafebabe0000003e001b0a000200030700040c000500060100106a617661
2f6c616e672f4f626a6563740100063c696e69743e010003282956090008
000907000a0c000b000c0100106a6176612f6c616e672f53797374656d01
00036f75740100154c6a6176612f696f2f5072696e7453747265616d3b08
000e01000e48656c6c6f2066726f6d202330310a001000110700120c0013
00140100136a6176612f696f2f5072696e7453747265616d010007707269
6e746c6e010015284c6a6176612f6c616e672f537472696e673b29560700
160100106363652f436c73436972634572723031010004436f646501000f
4c696e654e756d6265725461626c6501000a536f7572636546696c650100
11436c734369726345727230312e6a617661002100150002000000000001
000100050006000100170000002d000200010000000d2ab70001b2000712
0db6000fb10000000100180000000e00030000000400040005000c000600
01001900000002001a

Let's do this for of all of our class files and put them in a Data class:

final class Data {
  public static final String CLASS_01 = //
      """
      cafebabe0000003e001b0a000200030700040c000500060100106a617661\
      2f6c616e672f4f626a6563740100063c696e69743e010003282956090008\
      000907000a0c000b000c0100106a6176612f6c616e672f53797374656d01\
      00036f75740100154c6a6176612f696f2f5072696e7453747265616d3b08\
      000e01000e48656c6c6f2066726f6d202330310a001000110700120c0013\
      00140100136a6176612f696f2f5072696e7453747265616d010007707269\
      6e746c6e010015284c6a6176612f6c616e672f537472696e673b29560700\
      160100106363652f436c73436972634572723031010004436f646501000f\
      4c696e654e756d6265725461626c6501000a536f7572636546696c650100\
      11436c734369726345727230312e6a617661002100150002000000000001\
      000100050006000100170000002d000200010000000d2ab70001b2000712\
      0db6000fb10000000100180000000e00030000000400040005000c000600\
      01001900000002001a""";

  public static final String CLASS_02 = //
      (...)

  public static final String CLASS_03 = //
      (...)

  private Data() {}
}

I am using text blocks. Please note the new line escape character at the end of each line.

Using the MethodHandles.Lookup::defineClass method

OK, so our Data class contains the hex dumps of our classes. Let's use it to:

We will use the MethodHandles.Lookup::defineClass method. The following class is a thin wrapper around it:

class LookupWrapper {
  private final Lookup lookup = MethodHandles.lookup();

  public Object newInstance(String s) throws Exception {
    var hexFormat = HexFormat.of();

    var bytes = hexFormat.parseHex(s);

    var c = lookup.defineClass(bytes);

    var constructor = c.getConstructor();

    return constructor.newInstance();
  }
}

The lookup field is initialized with the value returned from the MethodHandles.lookup static method.

The newInstance method:

Let's use it with our three classes. Running the following program:

public class DynClass {
  public static void main(String[] args) throws Exception {
    var lookup = new LookupWrapper();

    System.out.println("new ClsCirErr01()");
    lookup.newInstance(Data.CLASS_01);

    System.out.println("new ClsCirErr02()");
    lookup.newInstance(Data.CLASS_02);

    System.out.println("new ClsCirErr03()");
    lookup.newInstance(Data.CLASS_03);
  }
}

Prints:

new ClsCirErr01()
Hello from #01
new ClsCirErr02()
Hello from #01
Hello from #02
new ClsCirErr03()
Hello from #01
Hello from #02
Hello from #03

Great! We have successfully created objects from the hex dump of their class files.

A quick note on loading order

For the Lookup.defineClass method to work any class dependency must have been previously loaded. In other words, order is important. For example, reversing the order from the previous example:

public class DynClassReverseOrder {
  public static void main(String[] args) throws Exception {
    var lookup = new LookupWrapper();

    System.out.println("new ClsCirErr03()");
    lookup.newInstance(Data.CLASS_03);

    System.out.println("new ClsCirErr02()");
    lookup.newInstance(Data.CLASS_02);

    System.out.println("new ClsCirErr01()");
    lookup.newInstance(Data.CLASS_01);
  }
}

Causes a NoClassDefFoundError to be thrown:

new ClsCirErr03()
Exception in thread "main" java.lang.NoClassDefFoundError: cce/ClsCircErr02
        at java.base/java.lang.ClassLoader.defineClass0(Native Method)
        at java.base/java.lang.System$2.defineClass(System.java:2346)
        at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2432)
        at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2409)
        at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1835)
        at cce.LookupWrapper.newInstance(LookupWrapper.java:20)
        at cce.DynClassReverseOrder.main(DynClassReverseOrder.java:13)
Caused by: java.lang.ClassNotFoundException: cce.ClsCircErr02
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        ... 7 more

How to edit the superclass in a class file?

I believe we can cause a ClassCircularityError by changing our hierarchy to the following:

class ClsCircErr03 extends ClsCircErr02 {}

class ClsCircErr02 extends ClsCircErr03 {}

Therefore we need to change the superclass of ClsCircErr02 from ClsCircErr01 to ClsCirErr03.

But where in the class file is the superclass declared? In other words, in what way must we edit the ClsCircErr02 hex dump to change its superclass?

We will have to look into the class file format specification.

The class file format

The class file format is defined in chapter 4 of the JVM Specification. Section 4.1 defines the ClassFile structure. Here is a relevant snippet:

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    (...)
}

At the end of the snippet we can see the super_class item. The JVM specification says the following about it:

For a class, the value of the super_class item either must be zero or must be a valid index into the constant_pool table.

So it seems we should focus on the constant pool.

Locating the constant pool in our class file

Before we go into more details about the constant pool let's first locate it in our hex dump.

From the previous section we can see that the constant pool is preceded by four items:

u4             magic;
u2             minor_version;
u2             major_version;
u2             constant_pool_count;

Given that:

We can reorganize the first two lines of the ClsCircErr02 hex dump:

cafebabe0000003e001b0a000200030700040c000500060100106363652f
436c734369726345727230310100063c696e69743e010003282956090008

Like so:

cafebabe   -- magic
0000       -- minor_version
003e       -- major_version
001b       -- constant_pool_count

-- constant pool start
0a000200030700040c000500060100106363652f
436c734369726345727230310100063c696e69743e010003282956090008

We can convert the hexadecimal values to decimal:

Locating the superclass name in the constant pool

We have located the start of the constant pool. Now we have to find the superclass information in it.

To make locating it easier let's get ourselves a "cheat sheet".

The JDK javap tool allows us to get a human-readable printout of the constant pool. Let's use with our ClsCircErr02.class file:

$ javap -verbose ClsCircErr02.class
(...)
public class cce.ClsCircErr02 extends cce.ClsCircErr01
  minor version: 0
  major version: 62
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #21                         // cce/ClsCircErr02
  super_class: #2                         // cce/ClsCircErr01
(...)

The actual output is much longer. The ellipses indicate suppressed parts.

Notice that both the minor and major version values matches the ones we found in the previous section. Good, this might indicate we are in the right direction.

We are interested in the super_class information. Its value is entry #2.

The javap output for the constant pool section is:

$ javap -verbose ClsCircErr02.class
(...)
Constant pool:
   #1 = Methodref          #2.#3          // cce/ClsCircErr01."<init>":()V
   #2 = Class              #4             // cce/ClsCircErr01
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               cce/ClsCircErr01
   #5 = Utf8               <init>
(...)

We can see that entry #2 is of type Class and references entry #4.

Entry #4 is of type Utf8 and its value is cce/ClsCirErr01.

So we must edit entry #4.

Let's go to our hex dump:

-- constant pool start
0a000200030700040c000500060100106363652f
436c734369726345727230310100063c696e69743e010003282956090008

From our "cheat sheet", entry #1 is a MethodRef constant. Its definition is:

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

It is made of:

Therefore, we must skip a total of five bytes. So our hex dump becomes:

-- constant pool start
0a00020003   -- Entry #1 (Methodref #2.#3)
0700040c000500060100106363652f
436c734369726345727230310100063c696e69743e010003282956090008

Entry #2 is a Class constant. Its definition is:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

We must skip three bytes:

-- constant pool start
0a00020003   -- Entry #1 (Methodref #2.#3)
070004       -- Entry #2 (Class #4)
0c000500060100106363652f
436c734369726345727230310100063c696e69743e010003282956090008

Entry #3 is a NameAndType constant. Its definition is:

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

We must skip five bytes:

-- constant pool start
0a00020003   -- Entry #1 (Methodref #2.#3)
070004       -- Entry #2 (Class #4)
0c00050006   -- Entry #3 (NameAndType #5:#6)
0100106363652f
436c734369726345727230310100063c696e69743e010003282956090008

Finally, entry #4 is a Utf8 constant. Its definition is:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

Applying this definition to our hex dump results in:

-- constant pool start
0a00020003   -- Entry #1 (Methodref #2.#3)
070004       -- Entry #2 (Class #4)
0c00050006   -- Entry #3 (NameAndType #5:#6)

-- Entry #4 (Utf8 cce/ClsCircErr01)
01                                -- tag
0010                              -- length
6363652f436c73436972634572723031  -- bytes

-- remaining entries
0100063c696e69743e010003282956090008

The length in hex bytes 0010 translates to the number 16 in decimal. As for the contents, using this ASCII table we can see that:

Therefore, we can safely assume the hex string:

6363652f436c73436972634572723031

Translates to:

cce/ClsCircErr01

Great! We have located the superclass name.

Andrew Binstock wrote a great introduction to the constant pool in this article for the Java magazine. If you want to learn more about the constant pool you should definitely read it.

Changing the superclass

OK, so we have located the superclass name in our hex dump. Let's confirm that we can change it. A way to test it is to have the ClsCircErr02 class extend java.lang.Object. In other words, change it so it is equivalent to the following Java code:

public class ClsCircErr02 {
  public ClsCircErr02() {
    System.out.println("Hello from #02");
  }
}

So if we manage to correctly change the superclass, creating an instance should print:

Hello from #02

As opposed to the current output:

Hello from #01
Hello from #02

All right, sounds reasonable enough, correct? The problem is, what is the Utf8 constant for the java/lang/Object string?

We can extract this information from the ClsCircErr01 class file. ClsCircErr01 extends java.lang.Object.

Using the javap tool:

$ javap -verbose ClsCircErr01.class
(...)
public class cce.ClsCircErr01
  minor version: 0
  major version: 62
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #21                         // cce/ClsCircErr01
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
(...)

We can see that, like the ClsCircErr02.class, the superclass name is in entry #4 of the constant pool. Going through the same steps of the previous section with the ClsCircErr01 class file we find that entry #4 is:

-- Entry #4 (Utf8 java/lang/Object)
01                                -- tag
0010                              -- length
6a6176612f6c616e672f4f626a656374  -- bytes

Both java/lang/Object and cce/ClsCircErr01 strings have the same length: 16 bytes. This is no coincidence. The cce.ClsCircErr0X names were chosen so they have the same length as java.lang.Object. So this is the reason for such weird name.

With the java/lang/Object information, let's edit the ClsCircErr02 hex dump. We will put the edited class file in a Edit class:

final class Edit {
  public static final String CLASS_02_OBJECT = //
      """
      cafebabe\
      0000\
      003e\
      001b\
      0a00020003\
      070004\
      0c00050006\
      0100106a6176612f6c616e672f4f626a656374\
      0100063c696e69743e010003282956090008\
      000907000a0c000b000c0100106a6176612f6c616e672f53797374656d01\
      00036f75740100154c6a6176612f696f2f5072696e7453747265616d3b08\
      000e01000e48656c6c6f2066726f6d202330320a001000110700120c0013\
      00140100136a6176612f696f2f5072696e7453747265616d010007707269\
      6e746c6e010015284c6a6176612f6c616e672f537472696e673b29560700\
      160100106363652f436c73436972634572723032010004436f646501000f\
      4c696e654e756d6265725461626c6501000a536f7572636546696c650100\
      11436c734369726345727230322e6a617661002100150002000000000001\
      000100050006000100170000002d000200010000000d2ab70001b2000712\
      0db6000fb10000000100180000000e00030000000400040005000c000600\
      01001900000002001a""";

  private Edit() {}
}

And we will create an instance of it using the following program:

public class ReplaceSuperclass {
  public static void main(String[] args) throws Exception {
    var lookup = new LookupWrapper();

    System.out.println("new ClsCirErr02()");
    lookup.newInstance(Edit.CLASS_02_OBJECT);
  }
}

Running it results in:

new ClsCirErr02()
Hello from #02

All right! We have successfully changed the superclass of our class.

Causing our ClassCircularityError

OK, I guess we are now ready to cause our ClassCircularityError. Or are we?

First, let's alter the superclass of the ClsCircErr02. To cause our error, it should point to ClsCircErr03. Let's put it in the Edit class. We call it CLASS_02_CIRCULAR:

final class Edit {
  public static final String CLASS_02_OBJECT = //
  (...)

  public static final String CLASS_02_CIRCULAR = //
      """
      cafebabe\
      0000\
      003e\
      001b\
      0a00020003\
      070004\
      0c00050006\
      0100106363652f436c73436972634572723033\
      0100063c696e69743e010003282956090008\
      000907000a0c000b000c0100106a6176612f6c616e672f53797374656d01\
      00036f75740100154c6a6176612f696f2f5072696e7453747265616d3b08\
      000e01000e48656c6c6f2066726f6d202330320a001000110700120c0013\
      00140100136a6176612f696f2f5072696e7453747265616d010007707269\
      6e746c6e010015284c6a6176612f6c616e672f537472696e673b29560700\
      160100106363652f436c73436972634572723032010004436f646501000f\
      4c696e654e756d6265725461626c6501000a536f7572636546696c650100\
      11436c734369726345727230322e6a617661002100150002000000000001\
      000100050006000100170000002d000200010000000d2ab70001b2000712\
      0db6000fb10000000100180000000e00030000000400040005000c000600\
      01001900000002001a""";

  private Edit() {}
}

Note how the entry #4 of the constant pool was changed:

before: 0100106363652f436c73436972634572723031
after:  0100106363652f436c73436972634572723033
                                             ^

And we will use our LookupWrapper to test it:

public class WillItThrow {
  public static void main(String[] args) throws Exception {
    var lookup = new LookupWrapper();

    System.out.println("new ClsCirErr02()");
    lookup.newInstance(Edit.CLASS_02_CIRCULAR);

    System.out.println("new ClsCirErr03()");
    lookup.newInstance(Data.CLASS_03);
  }
}

Unfortunately, it does not throw a ClassCircularityError. It fails with a NoClassDefFoundError instead:

new ClsCirErr02()
Exception in thread "main" java.lang.NoClassDefFoundError: cce/ClsCircErr03
        at java.base/java.lang.ClassLoader.defineClass0(Native Method)
        at java.base/java.lang.System$2.defineClass(System.java:2346)
        at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2432)
        at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2409)
        at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1835)
        at cce.LookupWrapper.newInstance(LookupWrapper.java:20)
        at cce.WillItThrow.main(WillItThrow.java:13)
Caused by: java.lang.ClassNotFoundException: cce.ClsCircErr03
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        ... 7 more

Which, of course, makes sense. Remember the "quick note on loading order" section. To load the modified class #02 requires class #03 to have been previously loaded. But to load class #03; class #02 is required.

Therefore, we will have to find another solution.

A custom class loader

We have been using the MethodHandles.Lookup::defineClass method to create our class definitions. As mentioned before, the Class javadocs indicates two other methods. Another option is to use the ClassLoader::defineClass method. So let's use it instead.

Here is a custom class loader:

final class CustomClassLoader extends ClassLoader {
  private final Map<String, String> data = new HashMap<>();

  public final Object newInstance(String name) throws Exception {
    var c = loadClass(name);

    var constructor = c.getConstructor();

    return constructor.newInstance();
  }

  public final void put(String name, String hexdump) {
    data.put(name, hexdump);
  }

  @Override
  protected final Class<?> findClass(String name) throws ClassNotFoundException {
    var hexdump = data.get(name);

    if (hexdump == null) {
      return super.findClass(name);
    } else {
      return load(name, hexdump);
    }
  }

  private Class<?> load(String name, String hexdump) {
    var hex = HexFormat.of();

    var bytes = hex.parseHex(hexdump);

    return defineClass(name, bytes, 0, bytes.length);
  }
}

Using the put method we can associate hex dumps to a class name. When it tries to load a class, it verifies if a hex dump exist for the requested class name. If a hex dump exists, it is used to create the class definition.

It also defines a newInstance method. It is just a convenience to load a class definition and create a new instance of it in one go.

To make sure our custom class loader works, let's test it:

public class CustomClassLoaderTest {
  public static void main(String[] args) throws Exception {
    var ourLoader = new CustomClassLoader();

    ourLoader.put("cce.ClsCircErr01", Data.CLASS_01);
    ourLoader.put("cce.ClsCircErr02", Data.CLASS_02);
    ourLoader.put("cce.ClsCircErr03", Data.CLASS_03);

    System.out.println("new ClsCirErr03()");
    ourLoader.newInstance("cce.ClsCircErr03");
  }
}

We associate the hex dumps to the fully qualified names of our three classes. We then create a new instance of class #03. Running it gives the following output:

new ClsCirErr03()
Hello from #01
Hello from #02
Hello from #03

It works correctly.

Second try: now with our custom class loader

OK, let's give it another try. This time using our custom class loader. Here is a program that does just that:

public class TryWithCustomClassLoader {
  public static void main(String[] args) throws Exception {
    var ourLoader = new CustomClassLoader();

    ourLoader.put("cce.ClsCircErr02", Edit.CLASS_02_CIRCULAR);
    ourLoader.put("cce.ClsCircErr03", Data.CLASS_03);

    System.out.println("new ClsCirErr03()");
    ourLoader.loadAndCreate("cce.ClsCircErr03");
  }
}

Let's run it:

new ClsCirErr03()
Exception in thread "main" java.lang.ClassCircularityError: cce/ClsCircErr03
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1013)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:875)
        at cce.CustomClassLoader.load(CustomClassLoader.java:43)
        at cce.CustomClassLoader.findClass(CustomClassLoader.java:34)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:588)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1013)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:875)
        at cce.CustomClassLoader.load(CustomClassLoader.java:43)
        at cce.CustomClassLoader.findClass(CustomClassLoader.java:34)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:588)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        at cce.CustomClassLoader.newInstance(CustomClassLoader.java:16)
        at cce.TryWithCustomClassLoader.main(TryWithCustomClassLoader.java:16)

Finally!

Let's try to understand our stack trace.

We can see that the ClassLoader::defineClass method is invoked twice.

The first invocation is for the ClsCircErr03 class. As class #03 extends class #02 it has to load the definition for ClsCircErr02.

However, we provided the circular definition of class ClsCircErr02. When the JVM tries to load it, it verifies that there is a circular hierarchy. So, it throws the ClassCircularityError.

Conclusion

I believe the majority of Java developers will never see a ClassCircularityError. Even when using libraries that create class definitions dynamically at runtime.

So we could say this experiment was rather "pointless". However it was a very good learning experience. We saw:

This was a fun post to write. I hope it was a pleasant read as well.

You can find the source code for all of the examples in this GitHub repository.