Context
I'm working on a Kotlin program which runs on the JVM and consumes large amounts of memory. While I do trust the garbage collector to (eventually) free memory used by objects which are no longer reachable, I don't trust future maintainers of the project (including my future self) – especially as the project progresses and becomes more evolved - to write the code in a way that makes sure that objects which are no longer needed are indeed unreachable.
So in order to reduce the risk, as part of my testing suite (which is already exhaustive with regards to the logic of the program's functionality) I'm also writing (or trying to write, with different degrees of success) various tests which aim to ensure that references aren't kept to objects which have run their course.
As this is quite difficult to do directly, the technique I'm using in the tests is to employ objects with finalizers, simulate the conditions when they're no longer needed, force garbage collection, and assert the finalizers have indeed run. (Note: my question is not about this technique per se, but if someone has comments or ideas for improvement or can propose alternatives – I'll be interested to hear!).
This generally works well, and can be shown to do the job, e.g. in TDD style: I write naive code which does the job as far as the business logic is concerned but doesn't take care of losing references to old objects, I write a test as described above, I make sure that the test fails, I add code to take care of memory (e.g., in simple cases, set references to null
), and then see that the test passes.
My question
For some reason, my tests don't always work (clarification: I don't mean that they fail non-deterministically; I mean that some of the tests consistently work and some consistently fail). The real examples from the project contain lots of intricate, proprietary details, but I've managed to boil it down to the following minimal example:
import kotlin.test.*
class FinalizationNotifier(val notify: () -> Unit) { protected fun finalize() = notify() }
class GcTest {
@Test fun `disposes of no-longer-used object`() {
var numTimesHasBeenDisposed = 0
// The following line allocates a (FinalizationNotifier) object, but keeps no reference to it
f(FinalizationNotifier { numTimesHasBeenDisposed++ }) // Note call to f, which in turn calls println
assertEquals(0, numTimesHasBeenDisposed) // Finalizer has not been run yet
System.gc() // Force garbage collection
Thread.sleep(1) // seems to be necessary to make sure finalizers have been run
assertEquals(1, numTimesHasBeenDisposed) // Finalizer has indeed been run
}
}
fun<T> f(value: T) { println(value) }
The test as written above passes, but if instead of calling f
I call println
directly – it fails (at the last assertion, i.e. the finalizer has not been run yet)! And my question is why that is, and/or what one should do in order to make sure (garbage gets collected and) finalizers are indeed run.
(Note: this is not about using println
or calling my own functions; my real code is much more complex, and does call my own functions, and has nothing to do with printing. This is just a minimal example I've managed to come up with which shows the inconsistency.)
In case it matters, I'm using Kotlin 1.5.10 on Java 11.0.6 (on macOS).
Update:
I've written the two tests side-by-side (in file T.kt
in package t
); below please find the source code, as well as the disassembly (obtained using javap -c
).
package t
import kotlin.test.*
class FinalizationNotifier(val notify: () -> Unit) { protected fun finalize() = notify() }
class GcTest {
@Test fun `disposes of no-longer-used object when calling own function`() {
var numTimesHasBeenDisposed = 0
f(FinalizationNotifier { numTimesHasBeenDisposed++ }) // Note call to f, which in turn calls println
assertEquals(0, numTimesHasBeenDisposed) // Finalizer has not been run yet
System.gc() // Force garbage collection
Thread.sleep(1) // seems to be necessary to make sure finalizers have been run
assertEquals(1, numTimesHasBeenDisposed) // Finalizer has indeed been run
}
@Test fun `disposes of no-longer-used object when calling println directly`() {
var numTimesHasBeenDisposed = 0
println(FinalizationNotifier { numTimesHasBeenDisposed++ }) // Note direct call to println
assertEquals(0, numTimesHasBeenDisposed) // Finalizer has not been run yet
System.gc() // Force garbage collection
Thread.sleep(1) // seems to be necessary to make sure finalizers have been run
assertEquals(1, numTimesHasBeenDisposed) // This fails for some reason
}
}
fun<T> f(value: T) { println(value) }
public final class t.FinalizationNotifier {
public t.FinalizationNotifier(kotlin.jvm.functions.Function0<kotlin.Unit>);
Code:
0: aload_1
1: ldc #10 // String notify
3: invokestatic #16 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: invokespecial #19 // Method java/lang/Object."<init>":()V
10: aload_0
11: aload_1
12: putfield #22 // Field notify:Lkotlin/jvm/functions/Function0;
15: return
public final kotlin.jvm.functions.Function0<kotlin.Unit> getNotify();
Code:
0: aload_0
1: getfield #22 // Field notify:Lkotlin/jvm/functions/Function0;
4: areturn
protected final void finalize();
Code:
0: aload_0
1: getfield #22 // Field notify:Lkotlin/jvm/functions/Function0;
4: invokeinterface #34, 1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
9: pop
10: return
}
Compiled from "T.kt"
final class t.GcTest$disposes of no-longer-used object when calling own function$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<kotlin.Unit> {
final kotlin.jvm.internal.Ref$IntRef $numTimesHasBeenDisposed;
t.GcTest$disposes of no-longer-used object when calling own function$1(kotlin.jvm.internal.Ref$IntRef);
Code:
0: aload_0
1: aload_1
2: putfield #13 // Field $numTimesHasBeenDisposed:Lkotlin/jvm/internal/Ref$IntRef;
5: aload_0
6: iconst_0
7: invokespecial #16 // Method kotlin/jvm/internal/Lambda."<init>":(I)V
10: return
public final void invoke();
Code:
0: aload_0
1: getfield #13 // Field $numTimesHasBeenDisposed:Lkotlin/jvm/internal/Ref$IntRef;
4: getfield #26 // Field kotlin/jvm/internal/Ref$IntRef.element:I
7: istore_1
8: aload_0
9: getfield #13 // Field $numTimesHasBeenDisposed:Lkotlin/jvm/internal/Ref$IntRef;
12: iload_1
13: iconst_1
14: iadd
15: putfield #26 // Field kotlin/jvm/internal/Ref$IntRef.element:I
18: return
public java.lang.Object invoke();
Code:
0: aload_0
1: invokevirtual #29 // Method invoke:()V
4: getstatic #35 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
7: areturn
}
Compiled from "T.kt"
final class t.GcTest$disposes of no-longer-used object when calling println directly$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<kotlin.Unit> {
final kotlin.jvm.internal.Ref$IntRef $numTimesHasBeenDisposed;
t.GcTest$disposes of no-longer-used object when calling println directly$1(kotlin.jvm.internal.Ref$IntRef);
Code:
0: aload_0
1: aload_1
2: putfield #13 // Field $numTimesHasBeenDisposed:Lkotlin/jvm/internal/Ref$IntRef;
5: aload_0
6: iconst_0
7: invokespecial #16 // Method kotlin/jvm/internal/Lambda."<init>":(I)V
10: return
public final void invoke();
Code:
0: aload_0
1: getfield #13 // Field $numTimesHasBeenDisposed:Lkotlin/jvm/internal/Ref$IntRef;
4: getfield #26 // Field kotlin/jvm/internal/Ref$IntRef.element:I
7: istore_1
8: aload_0
9: getfield #13 // Field $numTimesHasBeenDisposed:Lkotlin/jvm/internal/Ref$IntRef;
12: iload_1
13: iconst_1
14: iadd
15: putfield #26 // Field kotlin/jvm/internal/Ref$IntRef.element:I
18: return
public java.lang.Object invoke();
Code:
0: aload_0
1: invokevirtual #29 // Method invoke:()V
4: getstatic #35 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
7: areturn
}
Compiled from "T.kt"
public final class t.GcTest {
public t.GcTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public final void disposes of no-longer-used object when calling own function();
Code:
0: new #14 // class kotlin/jvm/internal/Ref$IntRef
3: dup
4: invokespecial #15 // Method kotlin/jvm/internal/Ref$IntRef."<init>":()V
7: astore_1
8: new #17 // class t/FinalizationNotifier
11: dup
12: new #19 // class "t/GcTest$disposes of no-longer-used object when calling own function$1"
15: dup
16: aload_1
17: invokespecial #22 // Method "t/GcTest$disposes of no-longer-used object when calling own function$1"."<init>":(Lkotlin/jvm/internal/Ref$IntRef;)V
20: checkcast #24 // class kotlin/jvm/functions/Function0
23: invokespecial #27 // Method t/FinalizationNotifier."<init>":(Lkotlin/jvm/functions/Function0;)V
26: invokestatic #33 // Method t/TKt.f:(Ljava/lang/Object;)V
29: iconst_0
30: invokestatic #39 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
33: aload_1
34: getfield #43 // Field kotlin/jvm/internal/Ref$IntRef.element:I
37: invokestatic #39 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
40: aconst_null
41: iconst_4
42: aconst_null
43: invokestatic #49 // Method kotlin/test/AssertionsKt.assertEquals$default:(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;ILjava/lang/Object;)V
46: invokestatic #54 // Method java/lang/System.gc:()V
49: lconst_1
50: invokestatic #60 // Method java/lang/Thread.sleep:(J)V
53: iconst_1
54: invokestatic #39 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
57: aload_1
58: getfield #43 // Field kotlin/jvm/internal/Ref$IntRef.element:I
61: invokestatic #39 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
64: aconst_null
65: iconst_4
66: aconst_null
67: invokestatic #49 // Method kotlin/test/AssertionsKt.assertEquals$default:(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;ILjava/lang/Object;)V
70: return
public final void disposes of no-longer-used object when calling println directly();
Code:
0: new #14 // class kotlin/jvm/internal/Ref$IntRef
3: dup
4: invokespecial #15 // Method kotlin/jvm/internal/Ref$IntRef."<init>":()V
7: astore_1
8: new #17 // class t/FinalizationNotifier
11: dup
12: new #65 // class "t/GcTest$disposes of no-longer-used object when calling println directly$1"
15: dup
16: aload_1
17: invokespecial #66 // Method "t/GcTest$disposes of no-longer-used object when calling println directly$1"."<init>":(Lkotlin/jvm/internal/Ref$IntRef;)V
20: checkcast #24 // class kotlin/jvm/functions/Function0
23: invokespecial #27 // Method t/FinalizationNotifier."<init>":(Lkotlin/jvm/functions/Function0;)V
26: astore_2
27: iconst_0
28: istore_3
29: getstatic #70 // Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_2
33: invokevirtual #75 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
36: iconst_0
37: invokestatic #39 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
40: aload_1
41: getfield #43 // Field kotlin/jvm/internal/Ref$IntRef.element:I
44: invokestatic #39 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
47: aconst_null
48: iconst_4
49: aconst_null
50: invokestatic #49 // Method kotlin/test/AssertionsKt.assertEquals$default:(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;ILjava/lang/Object;)V
53: invokestatic #54 // Method java/lang/System.gc:()V
56: lconst_1
57: invokestatic #60 // Method java/lang/Thread.sleep:(J)V
60: iconst_1
61: invokestatic #39 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
64: aload_1
65: getfield #43 // Field kotlin/jvm/internal/Ref$IntRef.element:I
68: invokestatic #39 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
71: aconst_null
72: iconst_4
73: aconst_null
74: invokestatic #49 // Method kotlin/test/AssertionsKt.assertEquals$default:(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;ILjava/lang/Object;)V
77: return
}
Compiled from "T.kt"
public final class t.TKt {
public static final <T> void f(T);
Code:
0: iconst_0
1: istore_1
2: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
5: aload_0
6: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
9: return
}