Imagine a bacon-wrapped Ferrari. Still not better than our free technical reports.

Solution to the smallest Java class file challenge

Last week we published a short Java challenge that required you to make javac generate the smallest Java class possible.

It got a pretty good response, quite a few of the readers decided to stretch their javac knowledge and try their hands on the challenge.

In this post, I’d like to show you a couple of solutions that I got from our readers.

Spoiler alert! If you want to see whether you can convince javac to generate a smaller class file, this is the right time to stop reading and open your terminal.


Baseline solution

Let’s start with a baseline measurement. Compile a simple, empty class file and check the size of the resulting class file.

Here’s a pretty empty Java class file:

class A {}

When we compile if with javac, it creates the A.class file, which we weight for the size:

$ javac A.java
$ wc -c A.class
      176 A.class

As we can see a straightforward way to get a small class file by compiling a small Java file gets us only into hundreds of bytes.

If you’re interested in exploring the resulting class file, you can see what is inside it by using the javap utility that comes with your JDK.

$ javap -v -p -c A

Classfile /Users/shelajev/repo/tmp/java-small-class-challenge/A.class
  Last modified Feb 6, 2017; size 176 bytes
  MD5 checksum 4a1401ad638511af830857a89c54a2bb
  Compiled from "A.java"
class A
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#10         // java/lang/Object."<init>":()V
   #2 = Class              #11            // A
   #3 = Class              #12            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               SourceFile
   #9 = Utf8               A.java
  #10 = NameAndType        #4:#5          // "<init>":()V
  #11 = Utf8               A
  #12 = Utf8               java/lang/Object
{
  A();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
}
SourceFile: "A.java"

Quite a lot of things, apparently.

A journey to smaller classes

So can we maybe shrink the source file even further? An interesting attempt came from Tagir Valeev, from JetBrains, master of the static code inspections, creator of numerous Java puzzles himself, and author of StreamEx library that enhances Java 8 Streams functionality, and much more!

Check this out, one can create an almost empty package-info file, that will be compiled into a package-info.class file, right? And since it’s almost empty it should be quite small. Let’s check it:

$ echo "package X;" > package-info.java
$ javac -Xpkginfo:always -g:none package-info.java 
$ wc -c package-info.class
      66 package-info.class

Note the crucial -g:none option we passed to the javac command. It tells javac not to output the debug symbols, making the resulting file much smaller.

If you’re curious, this is what the package-info.class looks like from the inside:

$ javap -p -c -v package-info.class
Classfile /Users/shelajev/repo/tmp/java-small-class-challenge/package-info.class
  Last modified Feb 6, 2017; size 66 bytes
  MD5 checksum 2846963a79ebed75a07bb26bb3be5a55
interface X.package-info
  minor version: 0
  major version: 52
  flags: ACC_INTERFACE, ACC_ABSTRACT, ACC_SYNTHETIC
Constant pool:
  #1 = Class              #3              // "X/package-info"
  #2 = Class              #4              // java/lang/Object
  #3 = Utf8               X/package-info
  #4 = Utf8               java/lang/Object
{
}

We can see that quite a bit of space is taken by the constants “X/package-info” and “java/lang/Object.”

If you didn’t know it beforehand, looking at the javap output should hint you that the package-info looks a lot like an interface (see the ACC_INTERFACE flag there?). Well, and that gives us the next step of the solution, let’s try an empty interface, hoping that we can specify a shorter name, so the constant “X/package-info” would be like 1–2 characters long.

Lo and behold, we have a very reasonable candidate for the smallest Java class file generated by javac.

$ echo "interface A {}" > A.java
$ javac -g:none A.java
$ wc -c A.class 
      53 A.class

If you look under the hood, you’ll see that it looks indeed just like the package-info, but with a shorter name:

$ javap -p -c -v A
Classfile /Users/shelajev/repo/tmp/java-small-class-challenge/A.class
  Last modified Feb 6, 2017; size 53 bytes
  MD5 checksum b33652e5fe31c274287cd991c85c9e8a
interface A
  minor version: 0
  major version: 52
  flags: ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
  #1 = Class              #3              // A
  #2 = Class              #4              // java/lang/Object
  #3 = Utf8               A
  #4 = Utf8               java/lang/Object
{
}

And thus, many of you stopped at 53 bytes and didn’t go further into the darkness of javac hacks.

However, this is just beginning. The next stop, which many of you uncovered and many considered to be the lower limit is 46 bytes.

Note that quite a large chunk of the class file now is the mandatory structure: the major/minor versions, modifier flags, etc. Perhaps the only thing that we can shrink further is the superclass reference which is the java/lang/Object.

Note that the 4th chapter of the Java Virtual Machine specification, the one that specifies the class file format, says:

“For an interface, the value of the super_class item must always be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_Class_info structure representing the class Object.”

So if we want to remove the reference, we need to be the java.lang.Object. This path leads us to the 46 bytes solution that makes use of the following hack.

package java.lang;
interface Object {}

And if you compile it without the debugging info, you get a tiny class file.

$ javac -g:none Object.java
$ wc -c Object.class
      46 Object.class

And this was for a long time the smallest class which we also thought of as the reference solution for the challenge.

38 bytes

Note that it is possible to generate a shorter class using not javac, but third party bytecode generators, like asm.

Then Andrei Pangin, a lead engineer at Odnoklassniki by Mail.ru, with a lot of experience as a VM engineer at Sun, came up with a brilliant solution that generates the 38 byte class file using javac only.

Here’s how it works, let’s start with a simple class that in theory should be short enough.

class A {}
class B extends A {}

The compiled result is not that impressive, class B takes 101 bytes, but if we look inside, we’ll notice that most of that space is taken by the default constructor.

$ javap -c -v -p B
Classfile /Users/shelajev/repo/tmp/java-small-class-challenge/B.class
  Last modified Feb 8, 2017; size 101 bytes
  MD5 checksum fd0d5bb8557f9b722d507f0dd64b29d7
class B extends A
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#7          // A."<init>":()V
   #2 = Class              #8             // B
   #3 = Class              #9             // A
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = NameAndType        #4:#5          // "<init>":()V
   #8 = Utf8               B
   #9 = Utf8               A
{
  B();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method A."<init>":()V
         4: return
}

If only there was a way to make javac emit the class without it. Naturally, to come up with the solution one has to know things about javac. For example that you can develop and plug code processors into the compilation process.

Here’s an example of an annotation processor that would remove the default constructor from the generated classes.

import com.sun.tools.javac.comp.Attr;
import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeTranslator;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class AnnProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set>? e<tends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element e : roundEnv.getRootElements()) {
            if (e.getSimpleName().contentEquals("B")) {
                JavacElements utils = (JavacElements) processingEnv.getElementUtils();
                JCTree.JCClassDecl cls = (JCTree.JCClassDecl) utils.getTree(e);
                JCTree.JCMethodDecl m = (JCTree.JCMethodDecl) cls.defs.head;
                cls.defs.head = new JCTree.JCMethodDecl(m.mods, m.name, m.restype, m.typarams, m.recvparam, m.params, m.thrown, m.body, m.defaultValue, m.sym) {
                    @Override
                    public void accept(Visitor v) {
                        if (v instanceof Attr || v instanceof TreeTranslator) {
                            super.accept(v);
                        }
                    }
                };
            }
        }
        return true;
    }
}

Now we just need to compile it, note that we only need javac to do it, and compile our classes passing the annotation processor as the target for the -processor option.

$ javac -cp $JAVA_HOME/lib/tools.jar AnnProcessor.java
$ javac -g:none -processor AnnProcessor B.java
$ wc -c B.class
      38 B.class

And we’re done.

Why is this solution exceptional? Because the resulting class can be loaded into the JVM, it’s valid and functional, meaning one can instantiate an object of B. And the solution creatively interprets the constraints of the challenge too.

Pushing the limits

If one is ready to stare into the darkness of the javac plugins, you can generate even smaller Java classes. Urs Keller, from a small software company called Revault, offered a solution that creates an amazingly empty class file that is only 30 bytes long.

First we need to get a javac plugin ready. Here’s the code for that:

import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.ClassType;
import com.sun.tools.javac.code.Types.DefaultTypeVisitor;
import com.sun.tools.javac.util.Name;

public class PluginImpl implements Plugin {
    private static final Name emptyName = new Name(null) {

        @Override
        public byte[] getByteArray() {
            return new byte[0];
        }

        @Override
        public byte getByteAt(int arg0) {
            return 0;
        }

        @Override
        public int getByteLength() {
            return 0;
        }

        @Override
        public int getByteOffset() {
            return 0;
        }

        @Override
        public int getIndex() {
            return 0;
        }

    };

    @Override
    public String getName() {
        return "PluginImpl";
    }

    @Override
    public void init(JavacTask task, String... arg1) {
        task.addTaskListener(new TaskListener() {

            @Override
            public void finished(TaskEvent taskEvent) {
            }

            @Override
            public void started(TaskEvent taskEvent) {
                if (taskEvent.getKind() == TaskEvent.Kind.GENERATE) {
                    if (taskEvent.getTypeElement() instanceof ClassSymbol) {
                        ClassSymbol s = (ClassSymbol) taskEvent.getTypeElement();
                        s.flatname = emptyName;
                        s.type.accept(new DefaultTypeVisitor<Type, Object>() {
                            @Override
                            public Type visitType(Type arg0, Object arg1) {
                                return arg0;
                            }

                            @Override
                            public Type visitClassType(ClassType arg0, Object arg1) {
                                arg0.supertype_field = arg0;
                                return super.visitClassType(arg0, arg1);
                            }
                        }, null);
                    }
                }
            }
        });
    }
}

And then we can compile an empty interface like the one below with the help of the plugin.

 $ javac -cp $JAVA_HOME/lib/tools.jar PluginImpl.java
 $ javac -g:none -processorpath . -Xplugin:PluginImpl O.java
 $ wc -c .class
       30 .class  

Note that we even remove the name of the class, so the output file is just .class. And naturally, no JVM will load it.

The current consensus is that you need at least 38 bytes for the class that is loadable.


Hope you enjoyed the challenge, learned a thing or two about javac and refreshed your knowledge of the JVM class file format.

If you have an interesting question about JVM, ping me, for example on Twitter: @shelajev, I’m always happy to chat, and maybe we can turn it into another challenge!


Read next: