掘金 后端 ( ) • 2024-05-05 11:12

highlight: androidstudio

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

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

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

2cab5001b85e4d69bc387e3cda79c8a.png 其中 nativeJavaCompiler 就是 JSR199,没错,就是最慢的那个

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

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

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

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

image.png

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

以下代码均使用 Java17

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

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

[点击展开/折叠代码块]
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 分析器查看火焰图长这样

image.png

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

  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就行:

    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

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实现是:

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 到参数:
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 中 覆盖如下方法:
    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说明也不是起到防止无限循环的作用,在内存层面也没有观察到减少内存占用,非常迷惑,由于解决方案更简单,甚至不用反射,这里就一并给出了

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 还快!:

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

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

1608442455089_1.gif

经过两天痛苦的排查,内存的泄露点竟然是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 (这个字段名单独写在了后面没有跟其他字段放在一起所以看漏了?),老样子,还是用反射解决:

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之后调用它:

Types types = Types.instance(((JavacTaskImpl) t).getContext());
clear(types);

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

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实例):

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<>()) (但是这违背了有序列表的约束,也许会出问题,也许不会)

原本的实现:

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();
    }
}

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

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代码如下:

[点击展开/折叠代码块]
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

清理编译后残余

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

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();
    }
}
@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的实现或者其他大佬的文章)

优化结果:

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