JavaCompiler API 为什么这么慢?如何将动态编译的速度优化一千倍
=======================================
众所周知,动态编译即在 java 运行时编译 java 代码的方法有常见的三种:
- JSR199 JavaCompiler API —— Java 1.6 以上 JDK tools.jar 提供(通常在JRE中不包含)
- ECJ Eclipse Java Compiler —— Eclipse 开发的适合 IDE 使用的可增量编译的 Java 编译器
- 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以外的类
[点击展开/折叠代码块]
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 分析器查看火焰图长这样
可以看到,运行时间占大头的是:
- BasicJavacTask#initPlugins 13%: 编译器插件和注解处理器,尽管这里是从一个空项目运行的测试,它依然会去寻找和加载插件和注解处理器(你可以设置
-proc:none
但它还是会占用很多时间) - JavaCompiler#initModules 11%: 如果你不需要需要模块功能,它还是会运行(除非
-source
设置为java9以下) - JavaFileManager#list 29% :用来扫描本地文件的部分,是的,它每次重复运行都会从硬盘扫描文件
- JavaFileManager#inferBinaryName 9% :用于确定文件的二进制名
总结一下,就是 JDK JavaCompiler API 慢的原因主要是:
- 没有缓存本地文件,每次都扫描一次 jar 包
- 对于我们不需要的模块功能总是运行
- 对于我们明确知道的或不需要的插件和注解处理器总是扫描
- 每一次解析都不能复用上次解析的结果,每一次都要重新将所有类和依赖解析一遍
我们依次解决每个问题:
解决问题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:关闭注解处理器
- 首先添加
-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();
- 在 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 还快!:
(看到那个±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
(这个字段名单独写在了后面没有跟其他字段放在一起所以看漏了?),老样子,还是用反射解决:
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的实现以及其他大佬的文章)
优化结果:
费这么一通功夫优化到了最后,JDK JavaCompiler API 终于又是世界最快的JAVA内存编译方法了
代码已发布在 github: fast-compiler
原文链接: https://juejin.cn/post/7364409181053321242
文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17781.html