JavaCompiler API 为什么这么慢?如何将动态编译的速度优化一千倍

众所周知,动态编译即在 java 运行时编译 java 代码的方法有常见的三种:

  1. JSR199 JavaCompiler API ------ Java 1.6 以上 JDK tools.jar 提供(通常在JRE中不包含)
  2. ECJ Eclipse Java Compiler ------ Eclipse 开发的适合 IDE 使用的可增量编译的 Java 编译器
  3. Janino ------ 一款超小、超快的 Java 编译器

一般来说,三者直接使用的运行速度是这样的(绝对值取决于具体的机器和实现,看相对值):

其中 nativeJavaCompiler 就是 JSR199,没错,就是最慢的那个

Janino 是大家的老熟人了,Spark 中就是使用 Janino 编译的 SQL 表达式,编译时间从原本的 50ms - 500ms 降到了 10 ms,在注解或配置文件里嵌入点 Java 代码的时候也会选择 Janino,又快又方便

但是令人遗憾的是,Janino 对 java 的特性支持是不完全的,并不能涵盖 java8 的全部特性,比如泛型、比如 lambda ,更高版本的特性支持也非常有限

最近在写表达式转 java 的编译器时就遇到这个问题,甚至不得已写了生成手动实现 lambda 代码的代码,一时间越想越气,难道 JDK JavaCompiler API 就这么慢?

接着在寻找 Janino 有没有办法支持 Lambda 的时候,从官网发现这么一段:

原来不是 JDK 慢,是 JDK 完全没对动态编译做优化,行吧,优化!

以下代码均使用 Java17

先用最简单的 API 用法跑跑 Profiling:

注意:此 Demo 的 MemoryFileManager 忽略了 spring 那种嵌套 jar 文件的加载和已经动态编译的类,即便不在乎性能也不能直接用在生产中,除非编译的源代码完全不依赖JDK以外的类
点击展开/折叠代码块

java 复制代码
import javax.tools.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class BenchmarkTest {
    private static final String source = """
            import java.util.function.BiFunction;
            public class LambdaContainer {
                public static BiFunction<Integer, Integer, Integer> getLambda() {
                    return (x, y) -> x + y;
                }
            }
            """;

    public static void main(String[] args) throws URISyntaxException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        while (true) {
            DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
            StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
            MemoryFileManager memoryFileManager = new MemoryFileManager(fileManager);
            compiler.getTask(null, memoryFileManager, diagnostics,
                    List.of("-source", "17", "-target", "17", "-encoding", "UTF-8"), null
                    , List.of(new MemoryInputJavaFileObject("LambdaContainer.java", source))).call();
            assert !memoryFileManager.getOutputs().isEmpty();
        }
    }


    static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
        private final List<MemoryOutputJavaFileObject> outputs = new ArrayList<>();

        protected MemoryFileManager(JavaFileManager fileManager) {
            super(fileManager);
        }

        @Override
        public String inferBinaryName(Location location, JavaFileObject file) {
            if (file instanceof BinaryJavaFileObject b) {
                String binaryName = b.getBinaryName();
                if (binaryName != null) {
                    return binaryName;
                }
            }
            return super.inferBinaryName(location, file);
        }

        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            if (kind == JavaFileObject.Kind.CLASS) {
                var fileObject = new MemoryOutputJavaFileObject(className);
                outputs.add(fileObject);
                return fileObject;
            }
            return super.getJavaFileForOutput(location, className, kind, sibling);
        }

        public List<MemoryOutputJavaFileObject> getOutputs() {
            return new ArrayList<>(outputs);
        }

        @Override
        public void close() throws IOException {
            super.close();
            outputs.clear();
        }
    }

    interface BinaryJavaFileObject extends JavaFileObject {
        String getBinaryName();
    }

    static class MemoryInputJavaFileObject extends SimpleJavaFileObject {
        private final String content;

        public MemoryInputJavaFileObject(String uri, String content) throws URISyntaxException {
            super(new URI("string:///" + uri), Kind.SOURCE);
            this.content = content;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return content;
        }
    }

    static class MemoryOutputJavaFileObject extends SimpleJavaFileObject implements BinaryJavaFileObject {
        private final ByteArrayOutputStream stream;
        private final String binaryName;

        public MemoryOutputJavaFileObject(String name) {
            super(URI.create("string:///" + name.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
            this.binaryName = name;
            this.stream = new ByteArrayOutputStream();
        }

        public byte[] toByteArray() {
            return stream.toByteArray();
        }

        public String getBinaryName() {
            return binaryName;
        }

        @Override
        public InputStream openInputStream() {
            return new ByteArrayInputStream(toByteArray());
        }

        @Override
        public ByteArrayOutputStream openOutputStream() {
            return this.stream;
        }
    }
}

通过 IDEA 分析器查看火焰图长这样

可以看到,运行时间占大头的是:

  1. BasicJavacTask#initPlugins 13%: 编译器插件和注解处理器,尽管这里是从一个空项目运行的测试,它依然会去寻找和加载插件和注解处理器(你可以设置-proc:none 但它还是会占用很多时间)
  2. JavaCompiler#initModules 11%: 如果你不需要需要模块功能,它还是会运行(除非-source设置为java9以下)
  3. JavaFileManager#list 29% :用来扫描本地文件的部分,是的,它每次重复运行都会从硬盘扫描文件
  4. JavaFileManager#inferBinaryName 9% :用于确定文件的二进制名

总结一下,就是 JDK JavaCompiler API 慢的原因主要是:

  1. 没有缓存本地文件,每次都扫描一次 jar 包
  2. 对于我们不需要的模块功能总是运行
  3. 对于我们明确知道的或不需要的插件和注解处理器总是扫描
  4. 每一次解析都不能复用上次解析的结果,每一次都要重新将所有类和依赖解析一遍

我们依次解决每个问题:

解决问题1:缓存扫描的jar包

这个问题是最好解决的,只要在 JavaFileManager实现里加个缓存就行,考虑到文件是每次发布固定的,甚至可以不用 Caffeine 这种专门的缓存,只定义一个静态的Map就行:

java 复制代码
    static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
        private static final Map<String, String> BINARY_NAME_CACHE = new ConcurrentHashMap<>();
        private static final Map<String, Iterable<JavaFileObject>> FILE_LIST_CACHE = new ConcurrentHashMap<>();

        @Override
        public String inferBinaryName(Location location, JavaFileObject file) {
            if (file instanceof BinaryJavaFileObject b) {
                String binaryName = b.getBinaryName();
                if (binaryName != null) {
                    return binaryName;
                }
            }
            return BINARY_NAME_CACHE.computeIfAbsent(location.getName() + file.toString(), k -> super.inferBinaryName(location, file));
        }

        @Override
        public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
            String key = location.getName() + ":" + packageName + ":" + kinds + ":" + recurse;

            return FILE_LIST_CACHE.computeIfAbsent(key, k -> {
                try {
                    return super.list(location, packageName, kinds, recurse);
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });
        }
//省略其他部分......
    }

解决问题2:关闭模块功能

-source java8 以上可以通过反射强行关闭 Module

java 复制代码
static {
    try {
        //编译这段代码需要添加 --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
        //运行时反射需要添加 --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
        Field maxLevel = com.sun.tools.javac.code.Source.Feature.MODULES.getDeclaringClass().getDeclaredField("maxLevel");
        maxLevel.setAccessible(true);
        removeFinal(maxLevel);
        maxLevel.set(Source.Feature.MODULES, Source.JDK1_2);
    } catch (Exception ignored) {
    }
}

其中removeFinal方法的Java17实现是:

java 复制代码
private static final Field MODIFIERS_FIELD;

static {
    Field field = null;
    try {
        //运行时反射需要添加 --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
        Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
        getDeclaredFields0.setAccessible(true);
        Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
        for (Field f1 : fields) {
            if (f1.getName().equals("modifiers")) {
                f1.setAccessible(true);
                field = f1;
                break;
            }
        }
    } catch (Throwable ignored) {
    }
    MODIFIERS_FIELD = field;
}

@SneakyThrows
public static void removeFinal(Field field) {
    if (MODIFIERS_FIELD == null) {
        throw new UnsupportedOperationException("Can't remove final modifier,please add " +
                                                "--add-opens=java.base/java.lang=ALL-UNNAMED " +
                                                "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED " +
                                                "to your jvm options");
    }
    MODIFIERS_FIELD.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}

ps: 如果上述代码在IDEA编译时出现 "java: 不允许在使用 --release 时从系统模块 jdk.compiler 导出程序包",取消勾选 构建、执行、部署〉编译器〉Java编译器〉使用'-release'选项进行交叉编译 即可,如果是springboot 程序,则需要在 pom properties 中添加 <maven.compiler.release></maven.compiler.release>

解决问题3:关闭注解处理器

  1. 首先添加 -proc:none 到参数:
java 复制代码
Boolean result = compiler.getTask(null, memoryFileManager, diagnostics,
        List.of("-source", "17", "-target", "17", "-encoding", "UTF-8","-proc:none"), null
        , List.of(new MemoryInputJavaFileObject("LambdaContainer.java", source))).call();
  1. 在 MemoryFileManager 中 覆盖如下方法:
java 复制代码
    static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
    /**
     * 关闭注解处理器
     */
    @Override
    public boolean hasLocation(Location location) {
        if (location == ANNOTATION_PROCESSOR_MODULE_PATH) {
            //返回true 交给 getServiceLoader 处理
            return true;
        }
        return super.hasLocation(location);
    }

    /**
     * 关闭注解处理器,通过空加载器减少扫描
     */
    @Override
    @SuppressWarnings("unchecked")
    public <S> ServiceLoader<S> getServiceLoader(Location location, Class<S> service) {
        // load EMPTY
        // 如果你一定需要注解处理器,那你就需要实现加载指定的注解处理器,可以考虑在这里实现, 向 ServiceLoader 传递一个自定义 ClassLoader 覆盖其中的 getResources 方法来加载明确已知的的注解处理器的 SPI 文件 从而优化性能,但是依然无法完全避免每次都重复读取 SPI 文件
        return (ServiceLoader<S>) ServiceLoader.loadInstalled(new Object() {}.getClass());
    }

//省略其他部分......
    }

解决问题4:复用解析结果

其实 JDK 中就有关于复用 JavaCompliler API 的类:com.sun.tools.javac.api.JavacTaskPool 它被用在 JShell 中

要使用它,我们要添加参数--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED

同时,我们发现com.sun.tools.javac.code.Types#candidatesCache 竟然是一个减慢速度的cache,并且内部实现是WeakHashMap说明也不是起到防止无限循环的作用,在内存层面也没有观察到减少内存占用,非常迷惑,由于解决方案更简单,甚至不用反射,这里就一并给出了

java 复制代码
public static void main(String[] args) throws URISyntaxException {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    JavacTaskPool javacTaskPool = new JavacTaskPool(1);
    while (true) {
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
        MemoryFileManager memoryFileManager = new MemoryFileManager(fileManager);

        List<MemoryOutputJavaFileObject> result = javacTaskPool.getTask(null, memoryFileManager, diagnostics,
                List.of("-source", "17", "-target", "17", "-encoding", "UTF-8", "-proc:none"), null
                , List.of(new MemoryInputJavaFileObject("LambdaContainer.java", source)), t -> {
                        Types types = Types.instance(((JavacTaskImpl) t).getContext());
                        // 这个 cache 会导致速度大量下降??,所以禁用
                        //noinspection rawtypes,unchecked
                        types.candidatesCache.cache = new HashMap() {
                            @Override
                            public Object put(Object key, Object value) {
                                return null;
                            }
                        };
                    if (Boolean.TRUE.equals(t.call())) {
                        return memoryFileManager.getOutputs();
                    }
                    return Collections.emptyList();
                });
        assert !result.isEmpty();
    }

然后你会惊奇的发现,JDK JavaCompliler API 比 Janino 还快!:

(看到那个±10206的方差了吗?没错,是伏笔)

然后你会更惊奇的发现,它竟然内!存!泄!露!了! 方差大的原因就是最后一次循环内存吃紧在不停GC

经过两天痛苦的排查,内存的泄露点竟然是JDK本K!这我还写个毛啊

好在,还是有解决办法的:

解决问题4+1:内存泄漏

1. 内存泄露最快的地方就是com.sun.tools.javac.code.Types.MembersClosureCache#nilScope ,很明显,是由于com.sun.tools.javac.code.Types#newRound 只清理了com.sun.tools.javac.code.Types.MembersClosureCache#_map没有清理 com.sun.tools.javac.code.Types.MembersClosureCache#nilScope (这个字段名单独写在了后面没有跟其他字段放在一起所以看漏了?),老样子,还是用反射解决:

java 复制代码
private static final Field MEMBERS_CACHE;
private static final Field NIL_SCOPE;

static {
    try {
        // 运行时反射需要添加 --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
        MEMBERS_CACHE = Types.class.getDeclaredField("membersCache");
        MEMBERS_CACHE.setAccessible(true);
        Class<?> aClass = Class.forName("com.sun.tools.javac.code.Types$MembersClosureCache");
        NIL_SCOPE = aClass.getDeclaredField("nilScope");
        NIL_SCOPE.setAccessible(true);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
/**
 * JDK bug, {@link Types#newRound} 在清除缓存时,没有清除{@link Types.MembersClosureCache#nilScope},会导致大量的内存泄露
 */
@SneakyThrows
@SuppressWarnings("JavadocReference")
public static void clear(Types types) {
    NIL_SCOPE.set(MEMBERS_CACHE.get(types), null);
}

在之前的Types.instance之后调用它:

java 复制代码
Types types = Types.instance(((JavacTaskImpl) t).getContext());
clear(types);

2. 其次就是一些内存泄露缓慢,但是分布广泛的地方:

java 复制代码
private static final Field CLASSES;
private static final Field SUB_SCOPES;
private static final Field LISTENERS;
private static final Field LIST_LISTENERS;


static {
// 运行时反射需要添加 --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
    try {
        LISTENERS = Scope.class.getDeclaredField("listeners");
        LISTENERS.setAccessible(true);
    } catch (NoSuchFieldException e) {
        throw new RuntimeException(e);
    }

    try {
        LIST_LISTENERS = Scope.ScopeListenerList.class.getDeclaredField("listeners");
        LIST_LISTENERS.setAccessible(true);
    } catch (NoSuchFieldException e) {
        throw new RuntimeException(e);
    }

    try {
        CLASSES = Symtab.class.getDeclaredField("classes");
        CLASSES.setAccessible(true);
    } catch (NoSuchFieldException e) {
        throw new RuntimeException(e);
    }

    try {
        SUB_SCOPES = Scope.CompoundScope.class.getDeclaredField("subScopes");
        SUB_SCOPES.setAccessible(true);
    } catch (NoSuchFieldException e) {
        throw new RuntimeException(e);
    }

}

@SneakyThrows
@SuppressWarnings({"unchecked", "rawtypes"})
public static void clear(Symtab symtab) {
    Map<Name, Map<Symbol.ModuleSymbol, Symbol.ClassSymbol>> classes = (Map) CLASSES.get(symtab);
    if (classes == null) {
        return;
    }

    classes.values().parallelStream()
            .forEach(value -> {
                for (Symbol.ClassSymbol classSymbol : value.values()) {
                    clear(classSymbol.members_field);
                }
            });
}

/**
 * {@link Scope#listeners} ,{@link Scope.ScopeListenerList#add} 没有清理 失效的 weakReference,累积之后会导致内存泄漏
 */
@SneakyThrows
@SuppressWarnings({"unchecked", "JavadocReference", "rawtypes"})
public static void clear(Scope scope) {
    if (scope == null) {
        return;
    }
    if (scope instanceof Scope.CompoundScope compoundScope) {
        ListBuffer<Scope> o1 = (ListBuffer) SUB_SCOPES.get(compoundScope);
        o1.forEach(ResourceUtil::clear);
    }
    Scope.ScopeListenerList listenerList = (Scope.ScopeListenerList) listeners.get(scope);
    if (listenerList == null) {
        return;
    }
    List<WeakReference<Scope.ScopeListener>> first = (List) list_listeners.get(listenerList);
    if (first == null || first.isEmpty()) {
        return;
    }

    List<WeakReference<Scope.ScopeListener>> current;

    // 使用for循环和tail手动遍历链表,移除失效的WeakReference
    List<WeakReference<Scope.ScopeListener>> prev = null;
    for (current = first; current != null; current = current.tail) {
        if (current.head == null || current.head.get() == null) {
            // 引用已失效
            if (prev != null) {
                prev.tail = current.tail;  // 移除当前节点
            } else {
                first = current.tail;  // 头节点失效,移动头指针
            }
        } else {
            prev = current;  // 更新前一个有效的节点
        }
    }
    if (first == null) {
        first = List.nil();
    }
    list_listeners.set(listenerList, first);
}

需要在任务运行前或者后执行(其实它泄露的很缓慢,并且这里的清理会影响不少性能,可以考虑 JavacTaskPool 不永久保留而是过一段时间扔掉换成新的JavacTaskPool实例):

java 复制代码
Symtab symtab = Symtab.instance(((JavacTaskImpl) t).getContext());
clear(symtab);

解决该内存泄漏有没有更好的办法呢?

有的!该内存泄漏的源头是com.sun.tools.javac.code.Scope.ScopeListenerList 设计不够合理,其中的 listeners 会不断累积已经失效的 WeakReference 并且无法及时处理,如果JDK能够修复此BUG就不用那么多反射了,或者使用 javaAgent 将该类的listeners字段的实现由 list 改为 Collections.newSetFromMap(new WeakHashMap<>()) (但是这违背了有序列表的约束,也许会出问题,也许不会)

原本的实现:

java 复制代码
public static class ScopeListenerList {

    List<WeakReference<ScopeListener>> listeners = List.nil();

    void add(ScopeListener sl) {
        listeners = listeners.prepend(new WeakReference<>(sl));
    }

    void symbolAdded(Symbol sym, Scope scope) {
        walkReferences(sym, scope, false);
    }

    void symbolRemoved(Symbol sym, Scope scope) {
        walkReferences(sym, scope, true);
    }

    private void walkReferences(Symbol sym, Scope scope, boolean isRemove) {
        ListBuffer<WeakReference<ScopeListener>> newListeners = new ListBuffer<>();
        for (WeakReference<ScopeListener> wsl : listeners) {
            ScopeListener sl = wsl.get();
            if (sl != null) {
                if (isRemove) {
                    sl.symbolRemoved(sym, scope);
                } else {
                    sl.symbolAdded(sym, scope);
                }
                newListeners.add(wsl);
            }
        }
        listeners = newListeners.toList();
    }
}

如果改成这样,一切就迎刃而解(大概):

java 复制代码
public static class ScopeListenerList {
    Set<ScopeListener> listeners = Collections.newSetFromMap(new WeakHashMap<>());

    void add(ScopeListener sl) {
        listeners.add(sl);
    }

    void symbolAdded(Symbol sym, Scope scope) {
        walkReferences(sym, scope, false);
    }

    void symbolRemoved(Symbol sym, Scope scope) {
        walkReferences(sym, scope, true);
    }

    private void walkReferences(Symbol sym, Scope scope, boolean isRemove) {
        for (ScopeListener sl : listeners) {
            if (isRemove) {
                sl.symbolRemoved(sym, scope);
            } else {
                sl.symbolAdded(sym, scope);
            }
        }
    }
}

更改上述字节码实现JavaAgent代码如下:
点击展开/折叠代码块

java 复制代码
import jakarta.annotation.Nonnull;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.jar.asm.*;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.JavaModule;

import java.io.IOException;
import java.io.StringReader;
import java.lang.instrument.Instrumentation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

import static net.bytebuddy.jar.asm.Opcodes.*;

public class MemoryLeakFixAgent {
    public static void premain(String agentArgs, Instrumentation inst) throws IOException {
        Properties properties = System.getProperties();
        if (agentArgs != null && !agentArgs.isBlank()) {
            properties.load(new StringReader(agentArgs
                    .replace(",", "\n")
                    .replace("\\", "\\\\")
            ));
        }
        AgentBuilder agent = new AgentBuilder.Default();
        if (properties.getProperty("debug") != null) {
            String out = properties.getProperty("outputDir");
            if (out != null) {
                agent = agent.with(new AgentBuilder.Listener.Adapter() {
                    @Override
                    public void onTransformation(@Nonnull TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, @Nonnull DynamicType dynamicType) {
                        try {
                            Path path = Path.of(out, typeDescription.getName() + ".class");
                            Files.createDirectories(path.getParent());
                            Files.write(path, dynamicType.getBytes());
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
            }
        }
        agent
                .type(ElementMatchers.nameContains("com.sun.tools.javac.code.Scope$ScopeListenerList"))
                .transform((builder, typeDescription, classLoader, module, domain) -> builder
                        .visit(new AsmVisitorWrapper.AbstractBase() {
                            @Nonnull
                            @Override
                            public ClassVisitor wrap(@Nonnull TypeDescription instrumentedType,
                                                     @Nonnull ClassVisitor classVisitor,
                                                     @Nonnull Implementation.Context implementationContext,
                                                     @Nonnull TypePool typePool,
                                                     @Nonnull FieldList<FieldDescription.InDefinedShape> fields,
                                                     @Nonnull MethodList<?> methods,
                                                     int writerFlags,
                                                     int readerFlags) {
                                return new ClassVisitor(ASM9, null) {
                                    @Override
                                    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                                        @SuppressWarnings("UnnecessaryLocalVariable")
                                        ClassVisitor classWriter = classVisitor;
                                        FieldVisitor fieldVisitor;
                                        MethodVisitor methodVisitor;
                                        classWriter.visit(V22, ACC_PUBLIC | ACC_SUPER, "com/sun/tools/javac/code/Scope$ScopeListenerList", null, "java/lang/Object", null);

                                        classWriter.visitSource("Scope.java", null);

                                        classWriter.visitNestHost("com/sun/tools/javac/code/Scope");

                                        classWriter.visitInnerClass("com/sun/tools/javac/code/Scope$ScopeListenerList", "com/sun/tools/javac/code/Scope", "ScopeListenerList", ACC_PUBLIC | ACC_STATIC);

                                        classWriter.visitInnerClass("com/sun/tools/javac/code/Scope$ScopeListener", "com/sun/tools/javac/code/Scope", "ScopeListener", ACC_PUBLIC | ACC_STATIC | ACC_ABSTRACT | ACC_INTERFACE);

                                        {
                                            fieldVisitor = classWriter.visitField(0, "listeners", "Ljava/util/Set;", "Ljava/util/Set<Lcom/sun/tools/javac/code/Scope$ScopeListener;>;", null);
                                            fieldVisitor.visitEnd();
                                        }
                                        {
                                            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
                                            methodVisitor.visitCode();
                                            Label label0 = new Label();
                                            methodVisitor.visitLabel(label0);
                                            methodVisitor.visitLineNumber(200, label0);
                                            methodVisitor.visitVarInsn(ALOAD, 0);
                                            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
                                            Label label1 = new Label();
                                            methodVisitor.visitLabel(label1);
                                            methodVisitor.visitLineNumber(201, label1);
                                            methodVisitor.visitVarInsn(ALOAD, 0);
                                            methodVisitor.visitTypeInsn(NEW, "java/util/WeakHashMap");
                                            methodVisitor.visitInsn(DUP);
                                            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/WeakHashMap", "<init>", "()V", false);
                                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/util/Collections", "newSetFromMap", "(Ljava/util/Map;)Ljava/util/Set;", false);
                                            methodVisitor.visitFieldInsn(PUTFIELD, "com/sun/tools/javac/code/Scope$ScopeListenerList", "listeners", "Ljava/util/Set;");
                                            methodVisitor.visitInsn(RETURN);
                                            methodVisitor.visitMaxs(3, 1);
                                            methodVisitor.visitEnd();
                                        }
                                        {
                                            methodVisitor = classWriter.visitMethod(0, "add", "(Lcom/sun/tools/javac/code/Scope$ScopeListener;)V", null, null);
                                            methodVisitor.visitCode();
                                            Label label0 = new Label();
                                            methodVisitor.visitLabel(label0);
                                            methodVisitor.visitLineNumber(204, label0);
                                            methodVisitor.visitVarInsn(ALOAD, 0);
                                            methodVisitor.visitFieldInsn(GETFIELD, "com/sun/tools/javac/code/Scope$ScopeListenerList", "listeners", "Ljava/util/Set;");
                                            methodVisitor.visitVarInsn(ALOAD, 1);
                                            methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Set", "add", "(Ljava/lang/Object;)Z", true);
                                            methodVisitor.visitInsn(POP);
                                            Label label1 = new Label();
                                            methodVisitor.visitLabel(label1);
                                            methodVisitor.visitLineNumber(205, label1);
                                            methodVisitor.visitInsn(RETURN);
                                            methodVisitor.visitMaxs(2, 2);
                                            methodVisitor.visitEnd();
                                        }
                                        {
                                            methodVisitor = classWriter.visitMethod(0, "symbolAdded", "(Lcom/sun/tools/javac/code/Symbol;Lcom/sun/tools/javac/code/Scope;)V", null, null);
                                            methodVisitor.visitCode();
                                            Label label0 = new Label();
                                            methodVisitor.visitLabel(label0);
                                            methodVisitor.visitLineNumber(208, label0);
                                            methodVisitor.visitVarInsn(ALOAD, 0);
                                            methodVisitor.visitVarInsn(ALOAD, 1);
                                            methodVisitor.visitVarInsn(ALOAD, 2);
                                            methodVisitor.visitInsn(ICONST_0);
                                            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/sun/tools/javac/code/Scope$ScopeListenerList", "walkReferences", "(Lcom/sun/tools/javac/code/Symbol;Lcom/sun/tools/javac/code/Scope;Z)V", false);
                                            Label label1 = new Label();
                                            methodVisitor.visitLabel(label1);
                                            methodVisitor.visitLineNumber(209, label1);
                                            methodVisitor.visitInsn(RETURN);
                                            methodVisitor.visitMaxs(4, 3);
                                            methodVisitor.visitEnd();
                                        }
                                        {
                                            methodVisitor = classWriter.visitMethod(0, "symbolRemoved", "(Lcom/sun/tools/javac/code/Symbol;Lcom/sun/tools/javac/code/Scope;)V", null, null);
                                            methodVisitor.visitCode();
                                            Label label0 = new Label();
                                            methodVisitor.visitLabel(label0);
                                            methodVisitor.visitLineNumber(212, label0);
                                            methodVisitor.visitVarInsn(ALOAD, 0);
                                            methodVisitor.visitVarInsn(ALOAD, 1);
                                            methodVisitor.visitVarInsn(ALOAD, 2);
                                            methodVisitor.visitInsn(ICONST_1);
                                            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/sun/tools/javac/code/Scope$ScopeListenerList", "walkReferences", "(Lcom/sun/tools/javac/code/Symbol;Lcom/sun/tools/javac/code/Scope;Z)V", false);
                                            Label label1 = new Label();
                                            methodVisitor.visitLabel(label1);
                                            methodVisitor.visitLineNumber(213, label1);
                                            methodVisitor.visitInsn(RETURN);
                                            methodVisitor.visitMaxs(4, 3);
                                            methodVisitor.visitEnd();
                                        }
                                        {
                                            methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "walkReferences", "(Lcom/sun/tools/javac/code/Symbol;Lcom/sun/tools/javac/code/Scope;Z)V", null, null);
                                            methodVisitor.visitCode();
                                            Label label0 = new Label();
                                            methodVisitor.visitLabel(label0);
                                            methodVisitor.visitLineNumber(216, label0);
                                            methodVisitor.visitVarInsn(ALOAD, 0);
                                            methodVisitor.visitFieldInsn(GETFIELD, "com/sun/tools/javac/code/Scope$ScopeListenerList", "listeners", "Ljava/util/Set;");
                                            methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Set", "iterator", "()Ljava/util/Iterator;", true);
                                            methodVisitor.visitVarInsn(ASTORE, 4);
                                            Label label1 = new Label();
                                            methodVisitor.visitLabel(label1);
                                            methodVisitor.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"java/util/Iterator"}, 0, null);
                                            methodVisitor.visitVarInsn(ALOAD, 4);
                                            methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Iterator", "hasNext", "()Z", true);
                                            Label label2 = new Label();
                                            methodVisitor.visitJumpInsn(IFEQ, label2);
                                            methodVisitor.visitVarInsn(ALOAD, 4);
                                            methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Iterator", "next", "()Ljava/lang/Object;", true);
                                            methodVisitor.visitTypeInsn(CHECKCAST, "com/sun/tools/javac/code/Scope$ScopeListener");
                                            methodVisitor.visitVarInsn(ASTORE, 5);
                                            Label label3 = new Label();
                                            methodVisitor.visitLabel(label3);
                                            methodVisitor.visitLineNumber(217, label3);
                                            methodVisitor.visitVarInsn(ILOAD, 3);
                                            Label label4 = new Label();
                                            methodVisitor.visitJumpInsn(IFEQ, label4);
                                            Label label5 = new Label();
                                            methodVisitor.visitLabel(label5);
                                            methodVisitor.visitLineNumber(218, label5);
                                            methodVisitor.visitVarInsn(ALOAD, 5);
                                            methodVisitor.visitVarInsn(ALOAD, 1);
                                            methodVisitor.visitVarInsn(ALOAD, 2);
                                            methodVisitor.visitMethodInsn(INVOKEINTERFACE, "com/sun/tools/javac/code/Scope$ScopeListener", "symbolRemoved", "(Lcom/sun/tools/javac/code/Symbol;Lcom/sun/tools/javac/code/Scope;)V", true);
                                            Label label6 = new Label();
                                            methodVisitor.visitJumpInsn(GOTO, label6);
                                            methodVisitor.visitLabel(label4);
                                            methodVisitor.visitLineNumber(220, label4);
                                            methodVisitor.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"com/sun/tools/javac/code/Scope$ScopeListener"}, 0, null);
                                            methodVisitor.visitVarInsn(ALOAD, 5);
                                            methodVisitor.visitVarInsn(ALOAD, 1);
                                            methodVisitor.visitVarInsn(ALOAD, 2);
                                            methodVisitor.visitMethodInsn(INVOKEINTERFACE, "com/sun/tools/javac/code/Scope$ScopeListener", "symbolAdded", "(Lcom/sun/tools/javac/code/Symbol;Lcom/sun/tools/javac/code/Scope;)V", true);
                                            methodVisitor.visitLabel(label6);
                                            methodVisitor.visitLineNumber(222, label6);
                                            methodVisitor.visitFrame(Opcodes.F_CHOP, 1, null, 0, null);
                                            methodVisitor.visitJumpInsn(GOTO, label1);
                                            methodVisitor.visitLabel(label2);
                                            methodVisitor.visitLineNumber(223, label2);
                                            methodVisitor.visitFrame(Opcodes.F_CHOP, 1, null, 0, null);
                                            methodVisitor.visitInsn(RETURN);
                                            methodVisitor.visitMaxs(3, 6);
                                            methodVisitor.visitEnd();
                                        }
                                        classWriter.visitEnd();
                                    }
                                };
                            }
                        }))
                .installOn(inst);
    }
}

使用 agent 时带上=debug,outputDir=E:\TEMP\agent 即可观察修改后的 class

清理编译后残余

如果重复编译一个文件,残余清不清理都是无所谓的,不过显然这在生产中并不可能,所以我们需要清理动态编译后存储在编译任务上下文中的对象:

java 复制代码
public static void main(String[] args) throws URISyntaxException {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    JavacTaskPool javacTaskPool = new JavacTaskPool(1);
    while (true) {
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
        MemoryFileManager memoryFileManager = new MemoryFileManager(fileManager);

        List<MemoryOutputJavaFileObject> result = javacTaskPool.getTask(null, memoryFileManager, diagnostics,
                List.of("-source", "17", "-target", "17", "-encoding", "UTF-8", "-proc:none"), null
                , List.of(new MemoryInputJavaFileObject("LambdaContainer.java", source)), t -> {
                    Context ctx = ((JavacTaskImpl) t).getContext();
                    Types types = Types.instance(ctx);
                    clear(types); // 如果用 agent 或者JDK修改了实现 就不用调用这个方法了
                    // 这个cache 会导致速度大量下降,所以禁用
                    //noinspection rawtypes,unchecked
                    types.candidatesCache.cache = new HashMap() {
                        @Override
                        public Object put(Object key, Object value) {
                            return null;
                        }
                    };
                    try {
                        if (Boolean.TRUE.equals(t.call())) {
                            return memoryFileManager.getOutputs();
                        }
                        return Collections.emptyList();
                    } finally {
                        //附加清理:清除已编译的软件包:
                        Symtab symtab = Symtab.instance(ctx);
                        Names names = Names.instance(ctx);
                        Symbol.ModuleSymbol module = symtab.java_base == symtab.noModule ? symtab.noModule
                                : symtab.unnamedModule;
                        Symbol.Completer completer = ClassFinder.instance(ctx).getCompleter();
                        List<MemoryOutputJavaFileObject> outputs = memoryFileManager.getOutputs();
                        for (MemoryOutputJavaFileObject output : outputs) {
                            String binaryName = output.getBinaryName();
                            Symbol.ClassSymbol aClass = symtab.getClass(module, names.fromString(binaryName));
                            if (aClass != null) {
                                for (Symbol.ClassSymbol value : remove(symtab, aClass.flatName()).values()) {
                                    value.packge().members_field = null;
                                    value.packge().completer = completer;
                                }
                            }
                        }
                        // 清理 Scope 中可能的未清理资源
                        clear(symtab);  // 如果用 agent 或者JDK修改了实现 就不用调用这个方法了
                    }
                });
        assert !result.isEmpty();
    }
}
java 复制代码
@NonNull
@SneakyThrows
@SuppressWarnings({"unchecked", "rawtypes"})
public static Map<Symbol.ModuleSymbol, Symbol.ClassSymbol> remove(Symtab symtab, Name flatName) {
    Map<Name, Map<Symbol.ModuleSymbol, Symbol.ClassSymbol>> classes = (Map) CLASSES.get(symtab);
    if (classes == null) {
        return Collections.emptyMap();
    }
    return Objects.requireNonNullElse(classes.remove(flatName), Collections.emptyMap());
}

至此,优化 JavaCompiler API 就结束了(JavaFileManager读取嵌套jar的部分就不赘述,已经放够多代码了,有兴趣的可以查找Drools的实现或者其他大佬的文章)

优化结果:

费这么一通功夫优化到了最后,JDK JavaCompiler API 终于又是世界最快的JAVA内存编译方法了

相关推荐
Penge6667 小时前
Go 接口编译期断言
后端
我是一颗柠檬7 小时前
【MySQL全面教学】MySQL面试高频考点汇总Day15(2026年)
数据库·后端·mysql·面试
橙淮7 小时前
并发编程(六)
java·jvm
拽着尾巴的鱼儿7 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影8 小时前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
Ceelog8 小时前
久坐党自救指南:屏幕前 8 小时,身体到底在经历什么
前端·后端
EntyIU8 小时前
JVM内存与GC笔记
java·jvm·笔记
XS0301069 小时前
并发编程 六
java·后端
yaoxin5211239 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道9 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试