Skip to content

Commit 8718cd0

Browse files
committed
Restore CurriedFields in ClassBuilder.
Now that the CallSite table is immortal, everything added to that is a memory leak, so we need to be judicious. Tests with many Bosk objects having large state trees can run out of memory, which is pretty annoying. The downside of curried fields is that final instance fields are not _really_ final, so they're generally slower than static finals and ConstantCallSites, but at the moment, OOMs caused by leaks seem to be a bigger problem (though unlikely in production, where there's only typically a single, long-lived Bosk object).
1 parent 8fae481 commit 8718cd0

File tree

2 files changed

+62
-5
lines changed

2 files changed

+62
-5
lines changed

bosk-core/src/main/java/works/bosk/bytecode/ClassBuilder.java

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
import java.io.IOException;
55
import java.io.PrintWriter;
66
import java.lang.invoke.CallSite;
7-
import java.lang.invoke.ConstantCallSite;
87
import java.lang.invoke.MethodHandles;
98
import java.lang.invoke.MethodType;
109
import java.lang.reflect.Constructor;
1110
import java.lang.reflect.InvocationTargetException;
1211
import java.lang.reflect.Method;
12+
import java.util.ArrayList;
13+
import java.util.List;
1314
import java.util.Map;
1415
import java.util.Objects;
1516
import java.util.concurrent.ConcurrentHashMap;
@@ -31,13 +32,16 @@
3132
import static java.lang.System.identityHashCode;
3233
import static java.lang.reflect.Modifier.isStatic;
3334
import static java.util.Objects.requireNonNull;
35+
import static java.util.stream.Collectors.joining;
3436
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
3537
import static org.objectweb.asm.Opcodes.ACC_FINAL;
38+
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
3639
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
3740
import static org.objectweb.asm.Opcodes.ACC_SUPER;
3841
import static org.objectweb.asm.Opcodes.ALOAD;
3942
import static org.objectweb.asm.Opcodes.CHECKCAST;
4043
import static org.objectweb.asm.Opcodes.DUP;
44+
import static org.objectweb.asm.Opcodes.GETFIELD;
4145
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
4246
import static org.objectweb.asm.Opcodes.IFEQ;
4347
import static org.objectweb.asm.Opcodes.IFNE;
@@ -49,6 +53,7 @@
4953
import static org.objectweb.asm.Opcodes.ISTORE;
5054
import static org.objectweb.asm.Opcodes.NEW;
5155
import static org.objectweb.asm.Opcodes.POP;
56+
import static org.objectweb.asm.Opcodes.PUTFIELD;
5257
import static org.objectweb.asm.Opcodes.RETURN;
5358
import static org.objectweb.asm.Opcodes.SWAP;
5459
import static org.objectweb.asm.Opcodes.V1_8;
@@ -72,6 +77,8 @@ public final class ClassBuilder<T> {
7277
private MethodBuilder currentMethod = null;
7378
private int currentLineNumber = -1;
7479

80+
private final List<CurriedField> curriedFields = new ArrayList<>();
81+
7582
/**
7683
* @param className The simple name of the generated class;
7784
* the actual name will be given the prefix <code>GENERATED_</code> to identify it as not corresponding to any source file
@@ -110,13 +117,21 @@ public void beginClass() {
110117
}
111118

112119
private void generateConstructor(StackWalker.StackFrame sourceFileOrigin) {
113-
MethodVisitor ctor = classVisitor.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
120+
String ctorParameterDescriptor = curriedFields.stream()
121+
.map(CurriedField::typeDescriptor)
122+
.collect(joining());
123+
MethodVisitor ctor = classVisitor.visitMethod(ACC_PUBLIC, "<init>", "(" + ctorParameterDescriptor + ")V", null, null);
114124
ctor.visitCode();
115125
Label label = new Label();
116126
ctor.visitLabel(label);
117127
ctor.visitLineNumber(sourceFileOrigin.getLineNumber(), label);
118128
ctor.visitVarInsn(ALOAD, 0);
119129
ctor.visitMethodInsn(INVOKESPECIAL, superClassName, "<init>", "()V", false);
130+
for (CurriedField field: curriedFields) {
131+
ctor.visitVarInsn(ALOAD, 0);
132+
ctor.visitVarInsn(ALOAD, field.slot());
133+
ctor.visitFieldInsn(PUTFIELD, slashyName, field.name(), field.typeDescriptor());
134+
}
120135
ctor.visitInsn(RETURN);
121136
ctor.visitMaxs(0, 0); // Computed automatically
122137
ctor.visitEnd();
@@ -196,10 +211,36 @@ public LocalVariable popToLocal(Type type) {
196211
* be accessible from the generated class)
197212
*/
198213
public void pushObject(String name, Object object, Class<?> type) {
214+
CurriedField field = curry(name, object, type);
215+
beginPush();
216+
methodVisitor().visitVarInsn(ALOAD, 0);
217+
methodVisitor().visitFieldInsn(GETFIELD, slashyName, field.name(), field.typeDescriptor());
218+
}
219+
220+
private CurriedField curry(String name, Object object, Class<?> type) {
199221
type.cast(object);
222+
for (CurriedField candidate: curriedFields) {
223+
if (candidate.value() == object) {
224+
return candidate;
225+
}
226+
}
200227

201-
// TODO: Use ConstantDynamic instead
202-
invokeDynamic(name, new ConstantCallSite(MethodHandles.constant(type, object)));
228+
int ctorParameterSlot = 1 + curriedFields.size();
229+
CurriedField result = new CurriedField(
230+
ctorParameterSlot,
231+
"CURRIED" + ctorParameterSlot + "_" + name,
232+
Type.getDescriptor(type),
233+
object);
234+
curriedFields.add(result);
235+
236+
classVisitor.visitField(
237+
ACC_PRIVATE | ACC_FINAL,
238+
result.name(),
239+
result.typeDescriptor(),
240+
null, null
241+
).visitEnd();
242+
243+
return result;
203244
}
204245

205246
public void invokeDynamic(String name, CallSite callSite) {
@@ -341,8 +382,9 @@ public T buildInstance() {
341382
Constructor<?> ctor = new CustomClassLoader()
342383
.loadThemBytes(dottyName, bytes)
343384
.getConstructors()[0];
385+
Object[] args = curriedFields.stream().map(CurriedField::value).toArray();
344386
try {
345-
return supertype.cast(ctor.newInstance());
387+
return supertype.cast(ctor.newInstance(args));
346388
} catch (InstantiationException | IllegalAccessException | VerifyError | InvocationTargetException e) {
347389
throw new AssertionError("Should be able to instantiate the generated class", e);
348390
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package works.bosk.bytecode;
2+
3+
/**
4+
* An object reference that should be passed to the constructor of a generated
5+
* class so that it can be referenced from method bodies via
6+
* {@link org.objectweb.asm.Opcodes#GETFIELD GETFIELD}.
7+
*
8+
* @param slot The parameter slot in which this will arrive in the constructor
9+
*/
10+
public record CurriedField(
11+
int slot,
12+
String name,
13+
String typeDescriptor,
14+
Object value
15+
) { }

0 commit comments

Comments
 (0)