February 8, 2017

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."":()V #2 = Class #11 // A #3 = Class #12 // java/lang/Object #4 = Utf8 #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 SourceFile #9 = Utf8 A.java #10 = NameAndType #4:#5 // "":()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."":()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."":()V #2 = Class #8 // B #3 = Class #9 // A #4 = Utf8 #5 = Utf8 ()V #6 = Utf8 Code #7 = NameAndType #4:#5 // "":()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."":()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 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.

Final Thoughts

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!

Additional Resources

Have you seen our blog series about reloading Java classes? You can find the first one here.

Looking for more interesting Java content? Check out our post on Java Puzzlers. It has three hard-to-solve problems that you'll love.

Read the Article