Add classfile-api module: java.lang.classfile (JEP 484) parse, generate, transform with timing instrumentation, ASM 9.11.1 comparison, version limits, JDK 24/25/26 API diff

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ
This commit is contained in:
2026-10-29 04:14:57 +00:00
co-authored by Claude Sonnet 5.5
parent eca73ad544
commit 41101bea40
37 changed files with 1763 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
$ java -cp classes:asm-9.11.1.jar... demo.ParseDemo (25.0.5+8-LTS)
== header ==
bytes : 1488
this : demo/Target
super : java/lang/Object
version : 69.0 (ClassFile.JAVA_25_VERSION = 69)
flags : public,super
constant pool : 69 slots
== fields ==
final,private name Ljava/lang/String;
private calls I
== methods ==
public <init>(Ljava/lang/String;)V
public,static fib(I)I
public,static sumTo(I)J
public greet(Ljava/lang/String;)Ljava/lang/String;
public pause(J)V
public,static boom()V
public,static explode([I)I
public calls()I
== class elements, in file order (the model is a stream of elements) ==
AccessFlags
ClassFileVersion
Superclass
Interfaces
field name
field calls
method <init>
method fib
method sumTo
method greet
method pause
method boom
method explode
method calls
SourceFile=Target.java
BoundInnerClassesAttribute
== fib(I)I, instruction by instruction ==
ILOAD_0
ICONST_2
IF_ICMPGE
ILOAD_0
GOTO
ILOAD_0
ICONST_1
ISUB
INVOKESTATIC demo/Target.fib(I)I
ILOAD_0
ICONST_2
ISUB
INVOKESTATIC demo/Target.fib(I)I
IADD
IRETURN
== invoke targets in the whole class ==
2 x demo/Target.fib
1 x indy:makeConcatWithConstants
1 x java/lang/IllegalStateException.<init>
1 x java/lang/Object.<init>
1 x java/lang/Thread.sleep
== a JDK class from the runtime image: java.lang.String ==
bytes 50116, major 69, methods 171, fields 11, interfaces 5
PASS Target.class is class-file version 69 (compiled with --release 25)
PASS Target has 8 methods (constructor + 7)
PASS fib calls itself twice
PASS greet() uses invokedynamic string concatenation
exit=0
+13
View File
@@ -0,0 +1,13 @@
$ java -cp classes:asm-9.11.1.jar... demo.GenerateDemo target/gen/demo (25.0.5+8-LTS)
generated 501 bytes, major version 69 (wrote target/gen/demo/Generated.class)
ClassFile.verify(bytes) -> 0 problem(s)
PASS verify() finds nothing wrong with the generated class
defineClass -> demo.Generated hidden=false findable by name=true
[defineClass] add(2,3)=5 hello()=Hello from generated bytecode sumTo(100)=5050 classify(-4)=negative classify(4)=non-negative
PASS defineClass: generated methods return the right values
defineHiddenClass -> name starts with 'demo.Generated/': true hidden=true distinct from the normal class=true
[hidden] add(2,3)=5 hello()=Hello from generated bytecode sumTo(100)=5050 classify(-4)=negative classify(4)=non-negative
PASS hidden: generated methods return the right values
Class.forName(hidden name) -> ClassNotFoundException (hidden classes cannot be found by name)
PASS hidden class is hidden, the other is not
exit=0
+106
View File
@@ -0,0 +1,106 @@
$ javap -c -p -v demo.Generated (the class GenerateDemo wrote; 25.0.5+8-LTS)
public class demo.Generated
minor version: 0
major version: 69
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // demo/Generated
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 5, attributes: 0
Constant pool:
#1 = Utf8 demo/Generated
#2 = Class #1 // demo/Generated
#3 = Utf8 <init>
#4 = Utf8 ()V
#5 = Utf8 java/lang/Object
#6 = Class #5 // java/lang/Object
#7 = NameAndType #3:#4 // "<init>":()V
#8 = Methodref #6.#7 // java/lang/Object."<init>":()V
#9 = Utf8 add
#10 = Utf8 (II)I
#11 = Utf8 hello
#12 = Utf8 ()Ljava/lang/String;
#13 = Utf8 Hello from generated bytecode
#14 = String #13 // Hello from generated bytecode
#15 = Utf8 sumTo
#16 = Utf8 (I)J
#17 = Utf8 classify
#18 = Utf8 (I)Ljava/lang/String;
#19 = Utf8 negative
#20 = String #19 // negative
#21 = Utf8 non-negative
#22 = String #21 // non-negative
#23 = Utf8 Code
#24 = Utf8 StackMapTable
#25 = Utf8 java/lang/String
#26 = Class #25 // java/lang/String
{
public demo.Generated();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static int add(int, int);
descriptor: (II)I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
public static java.lang.String hello();
descriptor: ()Ljava/lang/String;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #14 // String Hello from generated bytecode
2: areturn
public static long sumTo(int);
descriptor: (I)J
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=1
0: lconst_0
1: lstore_1
2: iconst_1
3: istore_3
4: iload_3
5: iload_0
6: if_icmpgt 20
9: lload_1
10: iload_3
11: i2l
12: ladd
13: lstore_1
14: iinc 3, 1
17: goto 4
20: lload_1
21: lreturn
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 4
locals = [ long, int ]
frame_type = 15 /* same */
public static java.lang.String classify(int);
descriptor: (I)Ljava/lang/String;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: ifge 9
4: ldc #20 // String negative
6: goto 11
9: ldc #22 // String non-negative
11: areturn
StackMapTable: number_of_entries = 2
frame_type = 9 /* same */
frame_type = 65 /* same_locals_1_stack_item */
stack = [ class java/lang/String ]
}
@@ -0,0 +1,19 @@
$ java -cp classes:asm-9.11.1.jar... demo.BrokenGenerate (25.0.5+8-LTS)
DROP_STACK_MAPS: verify() -> 1 problem(s)
Expecting a stackmap frame at branch target 8 in demo/Broken::pick(int) @1
DROP_STACK_MAPS: defineClass -> java.lang.VerifyError
Expecting a stackmap frame at branch target 8
Exception Details:
Location:
PASS a branching method built without stack maps fails verification
PASS the same code with default options verifies
ireturn in a String method: build() succeeded (126 bytes)
verify: Bad return type in demo/Wrong::bad() @1 (java/lang/String is not assignable from integer_type)
PASS verify() catches a wrongly typed return
PASS stack underflow is caught while building
underflow: build() threw java.lang.IllegalArgumentException
Operand stack underflow at bytecode offset 0 of method u()
swapped labels: build() succeeded (173 bytes), verify() -> 0 problem(s)
swapped labels: defineClass -> java.lang.ClassFormatError: Illegal exception table range in class file demo/Swapped
PASS the JVM rejects the swapped exception-table labels
exit=0
+31
View File
@@ -0,0 +1,31 @@
$ java -cp classes:asm-9.11.1.jar... demo.TransformDemo target/instrumented (25.0.5+8-LTS)
original 1488 bytes, naive 1799 bytes, with exception handler 2155 bytes
-- naive: time normal returns only (1799 bytes; verify() problems: 0)
PASS naive: time normal returns only: demo.Target here is a different Class object than the one on the class path
boom() threw java.lang.IllegalStateException: boom
explode() threw ArrayIndexOutOfBoundsException
fib recorded 177 call(s)
sumTo recorded 1 call(s)
greet recorded 1 call(s)
pause recorded 1 call(s)
boom recorded 0 call(s)
explode recorded 0 call(s)
PASS naive version never records the two methods that throw
PASS fib(10) makes 177 calls and 177 timings were recorded (recursion included)
-- full: also time when an exception leaves the method (2155 bytes; verify() problems: 0)
PASS full: also time when an exception leaves the method: demo.Target here is a different Class object than the one on the class path
boom() threw java.lang.IllegalStateException: boom
explode() threw ArrayIndexOutOfBoundsException
fib recorded 177 call(s)
sumTo recorded 1 call(s)
greet recorded 1 call(s)
pause recorded 1 call(s)
boom recorded 1 call(s)
explode recorded 1 call(s)
PASS exception-safe version records boom() and explode() once each
PASS fib still recorded 177 times
PASS sumTo and greet recorded once each
PASS pause(50) was recorded as at least 50 ms (>= 50,000,000 ns)
PASS fib(20) gives the same answer before and after
PASS sumTo(12345) gives the same answer before and after
exit=0
@@ -0,0 +1,121 @@
$ javap -c -p demo.Target (original, as javac produced it; fib and boom only)
public static int fib(int);
Code:
0: iload_0
1: iconst_2
2: if_icmpge 9
5: iload_0
6: goto 22
9: iload_0
10: iconst_1
11: isub
12: invokestatic #13 // Method fib:(I)I
15: iload_0
16: iconst_2
17: isub
18: invokestatic #13 // Method fib:(I)I
21: iadd
22: ireturn
public static void boom();
Code:
0: new #31 // class java/lang/IllegalStateException
3: dup
4: ldc #33 // String boom
6: invokespecial #35 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
9: athrow
$ javap -c -p demo.Target (naive transform: records on normal return only)
public static int fib(int);
Code:
0: invokestatic #74 // Method java/lang/System.nanoTime:()J
3: lstore_1
4: iload_0
5: iconst_2
6: if_icmpge 13
9: iload_0
10: goto 26
13: iload_0
14: iconst_1
15: isub
16: invokestatic #13 // Method fib:(I)I
19: iload_0
20: iconst_2
21: isub
22: invokestatic #13 // Method fib:(I)I
25: iadd
26: ldc #76 // String Target.fib
28: invokestatic #74 // Method java/lang/System.nanoTime:()J
31: lload_1
32: lsub
33: invokestatic #82 // Method demo/Timings.record:(Ljava/lang/String;J)V
36: ireturn
public static void boom();
Code:
0: invokestatic #74 // Method java/lang/System.nanoTime:()J
3: lstore_0
4: new #31 // class java/lang/IllegalStateException
7: dup
8: ldc #33 // String boom
10: invokespecial #35 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
13: athrow
$ javap -c -p demo.Target (full transform: plus a catch-all handler)
public static int fib(int);
Code:
0: invokestatic #74 // Method java/lang/System.nanoTime:()J
3: lstore_1
4: iload_0
5: iconst_2
6: if_icmpge 13
9: iload_0
10: goto 26
13: iload_0
14: iconst_1
15: isub
16: invokestatic #13 // Method fib:(I)I
19: iload_0
20: iconst_2
21: isub
22: invokestatic #13 // Method fib:(I)I
25: iadd
26: ldc #76 // String Target.fib
28: invokestatic #74 // Method java/lang/System.nanoTime:()J
31: lload_1
32: lsub
33: invokestatic #82 // Method demo/Timings.record:(Ljava/lang/String;J)V
36: ireturn
37: astore_3
38: ldc #76 // String Target.fib
40: invokestatic #74 // Method java/lang/System.nanoTime:()J
43: lload_1
44: lsub
45: invokestatic #82 // Method demo/Timings.record:(Ljava/lang/String;J)V
48: aload_3
49: athrow
Exception table:
from to target type
4 37 37 any
public static void boom();
Code:
0: invokestatic #74 // Method java/lang/System.nanoTime:()J
3: lstore_0
4: new #31 // class java/lang/IllegalStateException
7: dup
8: ldc #33 // String boom
10: invokespecial #35 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
13: athrow
14: astore_2
15: ldc #90 // String Target.boom
17: invokestatic #74 // Method java/lang/System.nanoTime:()J
20: lload_0
21: lsub
22: invokestatic #82 // Method demo/Timings.record:(Ljava/lang/String;J)V
25: aload_2
26: athrow
Exception table:
from to target type
4 14 14 any
@@ -0,0 +1,30 @@
$ javap -public java.lang.classfile.** on each JDK, then diff by class (24: OpenJDK Runtime Environment Temurin-24.0.2+12 (build 24.0.2+12), 26: OpenJDK Runtime Environment Temurin-26.0.2.1+1 (build 26.0.2.1+1))
=== JDK 24 -> JDK 25 ===
+ java.lang.classfile.ClassFile: public static final int JAVA_25_VERSION;
~ java.lang.classfile.attribute.UnknownAttribute
was: public interface java.lang.classfile.attribute.UnknownAttribute extends java.lang.classfile.Attribute<java.lang.classfile.attribute.UnknownAttribute>, java.lang.classfile.ClassElement, java.lang.classfile.MethodElement, java.lang.classfile.FieldElement {
now: public interface java.lang.classfile.attribute.UnknownAttribute extends java.lang.classfile.Attribute<java.lang.classfile.attribute.UnknownAttribute>, java.lang.classfile.ClassElement, java.lang.classfile.MethodElement, java.lang.classfile.FieldElement, java.lang.classfile.CodeElement {
+ java.lang.classfile.constantpool.ClassEntry: public abstract boolean matches(java.lang.constant.ClassDesc);
+ java.lang.classfile.constantpool.MethodTypeEntry: public abstract boolean matches(java.lang.constant.MethodTypeDesc);
+ java.lang.classfile.constantpool.ModuleEntry: public abstract boolean matches(java.lang.constant.ModuleDesc);
+ java.lang.classfile.constantpool.PackageEntry: public abstract boolean matches(java.lang.constant.PackageDesc);
+ java.lang.classfile.constantpool.StringEntry: public abstract boolean equalsString(java.lang.String);
+ java.lang.classfile.constantpool.Utf8Entry: public abstract boolean isFieldType(java.lang.constant.ClassDesc);
+ java.lang.classfile.constantpool.Utf8Entry: public abstract boolean isMethodType(java.lang.constant.MethodTypeDesc);
239 classes in old, 239 in new, 9 difference(s)
=== JDK 25 -> JDK 26 ===
+ java.lang.classfile.ClassFile: public static final int JAVA_26_VERSION;
239 classes in old, 239 in new, 1 difference(s)
=== is it a preview API on JDK 24? ===
$ javap -v java.lang.classfile.ClassFile | grep -ci PreviewFeature
24.0.2+12: 0
25.0.5+8-LTS: 0
26.0.2.1+1: 0
=== @since of ClassFile ===
jdk-24.0.2+12: * @since 24
jdk-25.0.5+8: * @since 24
jdk-26.0.2.1+1: * @since 24
+18
View File
@@ -0,0 +1,18 @@
$ java -cp classes:asm-9.11.1.jar... demo.AsmParse (25.0.5+8-LTS)
ASM sees major version 69
public <init>(Ljava/lang/String;)V
public,static fib(I)I
public,static sumTo(I)J
public greet(Ljava/lang/String;)Ljava/lang/String;
public pause(J)V
public,static boom()V
public,static explode([I)I
public calls()I
2 x demo/Target.fib
1 x indy:makeConcatWithConstants
1 x java/lang/IllegalStateException.<init>
1 x java/lang/Object.<init>
1 x java/lang/Thread.sleep
PASS ASM and java.lang.classfile list identical method lines
PASS ASM and java.lang.classfile count identical invoke targets
exit=0
+7
View File
@@ -0,0 +1,7 @@
$ java -cp classes:asm-9.11.1.jar... demo.AsmGenerate (25.0.5+8-LTS)
ASM generated 501 bytes; java.lang.classfile generated 501 bytes
PASS ASM output passes ClassFile.verify()
[ASM-generated] add(2,3)=5 hello()=Hello from generated bytecode sumTo(100)=5050 classify(-4)=negative classify(4)=non-negative
PASS ASM-generated: generated methods return the right values
PASS both generators emit the same method list
exit=0
@@ -0,0 +1,17 @@
$ java -cp classes:asm-9.11.1.jar... demo.AsmInstrument (25.0.5+8-LTS)
original 1488 bytes, ASM-instrumented 2152 bytes
PASS ASM output passes ClassFile.verify()
-- ASM AdviceAdapter version (2152 bytes; verify() problems: 0)
PASS ASM AdviceAdapter version: demo.Target here is a different Class object than the one on the class path
boom() threw java.lang.IllegalStateException: boom
explode() threw ArrayIndexOutOfBoundsException
fib recorded 177 call(s)
sumTo recorded 1 call(s)
greet recorded 1 call(s)
pause recorded 1 call(s)
boom recorded 1 call(s)
explode recorded 1 call(s)
PASS ASM version also records both throwing methods once
PASS ASM version: fib recorded 177 times
PASS ASM version: pause(50) recorded as at least 50 ms
exit=0
@@ -0,0 +1,22 @@
$ java -cp classes:asm-9.11.1.jar... demo.VersionLimits (25.0.5+8-LTS)
running on Java 25; ClassFile.latestMajorVersion() = 69; ASM Opcodes.V25=69, V28=72
major 69:
java.lang.classfile : parsed, 8 methods
ASM 9.11.1 : parsed, 8 methods
major 70:
java.lang.classfile : java.lang.IllegalArgumentException: Unsupported class file version: 70
ASM 9.11.1 : parsed, 8 methods
major 72:
java.lang.classfile : java.lang.IllegalArgumentException: Unsupported class file version: 72
ASM 9.11.1 : parsed, 8 methods
major 73:
java.lang.classfile : java.lang.IllegalArgumentException: Unsupported class file version: 73
ASM 9.11.1 : java.lang.IllegalArgumentException: Unsupported class file version 73.0
major 99:
java.lang.classfile : java.lang.IllegalArgumentException: Unsupported class file version: 99
ASM 9.11.1 : java.lang.IllegalArgumentException: Unsupported class file version 99.0
PASS major 69 parses with java.lang.classfile
PASS one version above the JDK's own is rejected by java.lang.classfile
PASS ASM 9.11.1 still reads major 72 (Java 28)
PASS ASM 9.11.1 rejects major 73
exit=0
+3
View File
@@ -0,0 +1,3 @@
Lines of code in the method(s) that do the job (non-blank, non-comment), counted by scripts/run-all.sh:
generate demo.Generated java.lang.classfile 32 ASM 63
timing instrumentation java.lang.classfile 61 ASM 43
+18
View File
@@ -0,0 +1,18 @@
--- run 1 of 3 ---
running on Java 25.0.5+8-LTS, ASM 9.11.1; INDICATIVE numbers, thread CPU time, shared 2-vCPU sandbox, no JMH
2000 classes from java.base, 7001 KiB; 4 warm-up rounds, median of 9
instructions seen: java.lang.classfile 478741, ASM 478741
parse + visit every instruction : java.lang.classfile 119.4 ms ASM 166.1 ms (ASM / classfile = 1.39)
parse + copy + write back : java.lang.classfile 38.6 ms ASM 78.9 ms (ASM / classfile = 2.05)
--- run 2 of 3 ---
running on Java 25.0.5+8-LTS, ASM 9.11.1; INDICATIVE numbers, thread CPU time, shared 2-vCPU sandbox, no JMH
2000 classes from java.base, 7001 KiB; 4 warm-up rounds, median of 9
instructions seen: java.lang.classfile 478741, ASM 478741
parse + visit every instruction : java.lang.classfile 63.9 ms ASM 166.2 ms (ASM / classfile = 2.60)
parse + copy + write back : java.lang.classfile 26.8 ms ASM 88.1 ms (ASM / classfile = 3.29)
--- run 3 of 3 ---
running on Java 25.0.5+8-LTS, ASM 9.11.1; INDICATIVE numbers, thread CPU time, shared 2-vCPU sandbox, no JMH
2000 classes from java.base, 7001 KiB; 4 warm-up rounds, median of 9
instructions seen: java.lang.classfile 478741, ASM 478741
parse + visit every instruction : java.lang.classfile 48.8 ms ASM 44.2 ms (ASM / classfile = 0.90)
parse + copy + write back : java.lang.classfile 18.8 ms ASM 101.6 ms (ASM / classfile = 5.39)
@@ -0,0 +1,22 @@
$ unzip -l spring-core-7.0.10.jar | grep 'META-INF/versions/24/.*\.class' | wc -l
9
$ unzip -l spring-core-7.0.10.jar | grep 'versions/24.*ClassFile'
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate$Source.class
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.class
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata$Builder.class
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.class
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMetadataReader.class
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.class
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMethodMetadata$Source.class
META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMethodMetadata.class
$ unzip -l spring-core-7.0.10.jar | grep -c 'org/springframework/asm/.*\.class' (the repackaged ASM is still there)
37
$ javap -v -cp <jar>!/META-INF/versions/24 ...ClassFileMetadataReader | grep java/lang/classfile (does it really call the JDK API?)
java/lang/classfile/ClassFile
java/lang/classfile/ClassModel
$ unzip -p spring-core-7.0.10.jar META-INF/MANIFEST.MF | grep -i multi-release
Multi-Release: true
@@ -0,0 +1,7 @@
$ java -cp classes:asm-9.11.1.jar... demo.AsmGotcha (25.0.5+8-LTS)
readerFlags = 0 -> java.lang.IllegalArgumentException
LocalVariablesSorter only accepts expanded frames (see ClassReader.EXPAND_FRAMES)
PASS the message names the missing flag
readerFlags = EXPAND_FRAMES -> 2152 bytes, verify() problems: 0
PASS with EXPAND_FRAMES the output verifies
exit=0