📜 ⬆️ ⬇️

How to rasp the class file

Usually when you compile a Java file, you get .class files of about the same size as the source. I was interested in whether it is possible to make a .class file, which is larger by a small source, much more than the source.

You can search for some short language constructs that are compiled into long bytecode chains, but I was not satisfied with the linear gain. I immediately thought about the compilation of finally-blocks: they already wrote about it on Habré . In short, for each finally-block, with a non-empty try-block, at least two variants are created in bytecode: for the case of normal completion of the try-block and for the case of completion with an exception. In the latter case, the exception is stored in a new local variable, the finally code is executed, then the exception is taken from the local variable and transferred. But what if you put try-finally inside the finally and so on? The result exceeded all expectations.

I compiled using Oracle javac 1.7.0.60 and 1.8.0.25, the results practically did not differ. The path for exclusion is formed even if in the try block there is absolutely nothing reprehensible. For example, assigning an integer constant to a local variable is two instructions iconst and istore , neither of which the specification says that they can throw an exception. So we will write:
class A {{ int a; try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { try {a=0;} finally { a=0; }}}}}}}}}}}} }} 

Adding a new non-trivial code to the innermost finally causes a code too large compile error, so we limit ourselves to this. If someone forgot, this is our initialization block , which is attached to each constructor. For our task, there is no point in declaring the method.

Such a source takes 336 bytes, and the resulting class-file rattled up to 6,571,429 bytes, that is, 19,557 times (let's call it a dragging factor). Even if you disable all debugging information with -g: none, the class file weighs 6,522,221 bytes, which is only slightly smaller. Let's see what's inside with the javap utility.
')
Pool of constants

The pool of constants turned out to be small: only 16 records. In essence, everything you need is the following: attribute names of the Code type, class name, Java file, reference to the constructor of the parent class Object, etc. When you turn off debug information, three entries disappear: the attribute names LineNumberTable, SourceFile, and the A.java value for the SourceFile attribute .

Code

The default constructor code was 64507 bytes, almost resting on the maximum allowed limit. It begins with normal execution:
Code
  0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: iconst_0 5: istore_1 6: iconst_0 7: istore_1 8: iconst_0 9: istore_1 10: iconst_0 11: istore_1 12: iconst_0 13: istore_1 14: iconst_0 15: istore_1 16: iconst_0 17: istore_1 18: iconst_0 19: istore_1 20: iconst_0 21: istore_1 22: iconst_0 23: istore_1 24: iconst_0 25: istore_1 26: iconst_0 27: istore_1 28: iconst_0 29: istore_1 30: goto 38 

That is, the parent class constructor is called, and then the unit is written 13 times in the first local variable. After this, a long goto chain begins, which bypasses all other copies of finally: 30-> 38-> 58-> 104-> 198-> 388-> 770-> 1536-> 3074-> 7168-> 15358-> 31740- > 64506, and at 64506 we find the long-awaited return statement.

In between these goto, all possible combinations of normal and exceptional terminations of each try block. Unexpectedly, for each finally handling an exception, a new local variable is created to hold the exception, even if the blocks are mutually exclusive. Because of this, the code requires 4097 local variables. Some statistics on the instructions:


Plus one aload_0, one invokespecial and one return - a total of 32765 instructions. Those interested can draw a control flow graph and hang it on the wall.

Exception table

The exception table contains entries of the form (start_pc, end_pc, handler_pc, catch_type) and tells the virtual machine "if, when executing instructions from the address start_pc to the address end_pc, an exception of the type catch_type occurred, then transfer control to the address handler_pc". In this case, catch_type is equal to any, that is, any type of exception. The records in table 8188 and it takes about the same as the code - about 64 kilobytes. The beginning looks like this:
  from to target type 26 28 33 any 24 26 41 any 42 44 49 any 49 51 49 any 22 24 61 any 


Line number table

The line number table is debugging information that matches the addresses of the bytecode instructions to line numbers in the source code. It has 12,288 entries and most often comes across links to the line with the innermost finally. It takes about 48 kilobytes.

Stackmaptable

Where did the rest of the place go? It was taken by the StackMapTable table, which is needed to verify the class file. If it is very rough, for each branch point in the code, this table contains the types of elements in the stack and the types of local variables at that point. Since we have a lot of local variables and branch points too, the size of this table grows quadratically with the size of the code. If local variables were reused for exceptions in non-intersecting branches, they would need only 13 and the StackMapTable table would be much more modest in size.

Frighten further

Is it possible to ramp the class file even more? Of course, you can dig out a method containing nested try-finally. But the compiler may well do it for us. Recall that the initialization block is glued to each constructor automatically. It is enough to add a lot of empty constructors with different signatures to the code. Be careful here, otherwise the compiler runs out of memory. Well, you can modestly write this by packing the code into one line:

 class A{{int a;try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{a=0;}}}}}}}}}}}}}A(){}A(int a){}A(char a){}A(double a){}A(float a){}A(long a){}A(short a){}A(boolean a){}A(String a){}A(Integer a){}A(Float a){}A(Short a){}A(Long a){}A(Double a){}A(Boolean a){}A(Character a){}} 

Here I have 16 constructors, the source is 430 bytes . After compilation, we have 104,450,071 bytes ; the coefficient of haul was 242 907 . And this is not the limit!

Source: https://habr.com/ru/post/245333/


All Articles