wasyl
02/05/2021, 8:49 AMfun test() {
var foo: Foo? = Foo()
println(foo)
runBlock { foo = null }
}
private fun runBlock(block: () -> Unit) {
block()
}
class Foo
So I have var foo
, which I use in println()
and then null it out in another lambda. The compiler generates the following (decompiled) code:
final ObjectRef foo = new ObjectRef();
foo.element = new Foo();
Foo var2 = (Foo)foo.element;
boolean var3 = false;
System.out.println(var2);
Note the Foo var2 = (Foo)foo.element;
which is used in println
. It is somewhat unexpected, e.g. if Foo
is heavy I may want it to be GC-ed after foo = null
is executed. It won’t be, because var2
is keeping it. Why is necessary that var2
exists, and would it make sense to clear it after it’s used?Ilmir Usmanov [JB]
02/05/2021, 9:07 AMfoo
is captured in a lambda, which might be stored is variable, so, it assumes, that it is safer to hold a reference until the lambda is executed.wasyl
02/05/2021, 9:10 AMprintln(foo)
is executed and the var2
is never going to be used anymore. Can you give an example of when this var2
is needed?println(foo)
after runBlock { }
, then I still see the variable generated:
Foo var2 = (Foo)foo.element;
boolean var3 = false;
System.out.println(var2);
but it still captures foo.element
right before printing.Ilmir Usmanov [JB]
02/05/2021, 9:16 AMObjectRef
does not consume much memory - it is essentially object header + reference to another object. So, it is not a big deal.wasyl
02/05/2021, 9:20 AMSystem.out.println((Foo)foo.element);
or
Foo var2 = (Foo)foo.element;
System.out.println(var2);
var2 = null
Ilmir Usmanov [JB]
02/05/2021, 9:25 AMprintln
or something?wasyl
02/05/2021, 9:29 AMclass Foo
fun test() {
var foo: Foo? = Foo()
println(foo)
runBlock { foo = null }
}
private fun runBlock(block: () -> Unit) {
block()
}
And the bytecode for the method is
public final static test()V
L0
LINENUMBER 6 L0
NEW kotlin/jvm/internal/Ref$ObjectRef
DUP
INVOKESPECIAL kotlin/jvm/internal/Ref$ObjectRef.<init> ()V
ASTORE 0
ALOAD 0
NEW com/example/Foo
DUP
INVOKESPECIAL com/example/Foo.<init> ()V
PUTFIELD kotlin/jvm/internal/Ref$ObjectRef.element : Ljava/lang/Object;
L1
LINENUMBER 7 L1
ALOAD 0
GETFIELD kotlin/jvm/internal/Ref$ObjectRef.element : Ljava/lang/Object;
CHECKCAST com/example/Foo
ASTORE 1
L2
ICONST_0
ISTORE 2
L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 1
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
L4
L5
LINENUMBER 8 L5
NEW com/example/SpecSpecKt$test$1
DUP
ALOAD 0
INVOKESPECIAL com/example/SpecSpecKt$test$1.<init> (Lkotlin/jvm/internal/Ref$ObjectRef;)V
CHECKCAST kotlin/jvm/functions/Function0
INVOKESTATIC com/example/SpecSpecKt.runBlock (Lkotlin/jvm/functions/Function0;)V
L6
LINENUMBER 9 L6
RETURN
L7
LOCALVARIABLE foo Lkotlin/jvm/internal/Ref$ObjectRef; L1 L7 0
MAXSTACK = 3
MAXLOCALS = 3
Ilmir Usmanov [JB]
02/05/2021, 9:33 AMGETFIELD kotlin/jvm/internal/Ref$ObjectRef.element : Ljava/lang/Object;
CHECKCAST com/example/Foo
ASTORE 1
This is because of different type. The compiler does not create a 'new' variable. It uses additional slot, so it does not cast the variable every time the variable is accessed.INVOKESPECIAL com/example/Foo.<init> ()V
without ASTORE
Edit: I really hate slack visiwig redactor.wasyl
02/05/2021, 9:38 AMThis is because of different typeOkay but if I write
println(foo)
I see generated
Foo var1 = (Foo)foo.element;
boolean var2 = false;
System.out.println(var1);
But if I have a function with the same type:
fun useFoo(foo: Foo?) = println(foo)
then generated code is not using intermediate variable:
useFoo((Foo)foo.element);
but it still does the same amount of castinga coroutine used to hold reference to potentially all local variables, which lead to memory leaksis applicable in this scenario as well, and we can potentially hold on to massive objects even though looking at the code would suggest that we don’t. If we use
suspend fun
it’s not difficult to imagine runBlock
running for a long time, in which case Foo
would be kept as wellIlmir Usmanov [JB]
02/05/2021, 9:52 AMboolean var2 = false;
has no effect, unless you use a debugger and it shows, that you are inside an inline function - in this case, println
.
I changed useFoo
to be inline as well and lo and behold
GETFIELD kotlin/jvm/internal/Ref$ObjectRef.element : Ljava/lang/Object;
CHECKCAST Foo
ASTORE 1
BTW, what is the problem? The compiler does not generate code, which creates additional memory allocations (one ObjectRef is hardly an issue for a generational GC). If you do not want to allocate even this amount of additional memory, you can declare runBlock
inline.
The last piece of advice - take decompiled code with a pound of salt - it never tells the whole truth. The bytecode tells much more. For example, the decompiler never generates correct names for inline variables, but still shows them as boolean var2 = false
.it’s not difficult to imagineIt's not difficult to imaginerunning for a long time, in which caserunBlock
would be kept as wellFoo
runBlock
stores the lambda parameter and all its closure should live as long as the lambda is stored.wasyl
02/05/2021, 10:01 AMIt’s not difficult to imagineNo, but that’s relatively easy to spot when looking at the code, it’s just not surprising, as the same thing happens in plain Java. Just to circle back, I’m not complaining aboutstores the lambda parameter and all its closure should live as long as the lambda is stored.runBlock
ObjectRef
or even that the compiler generates any variables she wants. It’s just that current behaviour broke my assumption about JVM being able to GC an object. In all the code I wrote in Kotlin (and could read), I cleared the references. But the JVM couldn’t GC Foo
instance because it was kept as a result of what Kotlin compiler did internally.Ilmir Usmanov [JB]
02/05/2021, 10:09 AMif
statement) or is captured in lambda. Additionally, there is difference in inline lambdas and ordinary ones.
using an object might prevent it from being GCed after all user-written references are clearedClearing an object harms in this particular case, since it forces the variable to be captured.
wasyl
02/05/2021, 10:11 AMClearing an object harms in this particular case, since it forces the variable to be captured.I want to be extra clear, I understand that
ObjectRef
is needed 100% and it doesn’t cause any harm here. Because when doing foo = null
it will do
L0
LINENUMBER 11 L0
ALOAD 0
GETFIELD com/example/SpecSpecKt$test$1.$foo : Lkotlin/jvm/internal/Ref$ObjectRef;
ACONST_NULL
CHECKCAST com/example/Foo
PUTFIELD kotlin/jvm/internal/Ref$ObjectRef.element : Ljava/lang/Object;
so there’s no longer any hard reference to Foo
object and it should be GC-edIlmir Usmanov [JB]
02/05/2021, 10:12 AMwasyl
02/05/2021, 10:12 AMprintln(foo)
before runBlock { foo = null }
test()
scopeIlmir Usmanov [JB]
02/05/2021, 10:13 AMwasyl
02/05/2021, 10:15 AMprintln(foo)
is there, if you comment it out it will pass. Now I understand there might be some optimisations happening but if I understand what the bytecode does it shouldn’t be the caseIlmir Usmanov [JB]
02/05/2021, 10:18 AMfun test() {
var foo: Foo? = Foo()
println(foo)
runBlock { foo = null }
System.gc()
repeat(10) {
println("Try")
Thread.sleep(10)
System.gc()
}
}
private fun runBlock(block: () -> Unit) {
block()
}
class Foo {
fun finalize() {
println("Cleanup")
}
}
fun main() {
test()
}
And got
Foo@7cc355be
Try
Try
Cleanup
Try
Try
Try
Try
Try
Try
Try
Try
So GC does what it should - it cleans the object up after just second call of System.gc()
.wasyl
02/05/2021, 10:22 AMIlmir Usmanov [JB]
02/05/2021, 10:26 AMwasyl
02/05/2021, 10:37 AMthere is a difference between captured variables in coroutines and ordinary codeThe thing I have is reproducible both with and without coroutines
Foo
. But if you put the System.gc(); repeat { }
part in a separate function, it doesn’t anymore:
fun test() {
var foo: Foo? = Foo()
println(foo)
runBlock { foo = null }
gc()
}
fun gc() {
System.gc()
repeat(10) {
println("Try")
Thread.sleep(10)
System.gc()
}
}
private fun runBlock(block: () -> Unit) {
block()
}
class Foo {
fun finalize() {
println("Cleanup")
}
}
fun main() {
test()
}
prints
Foo@5acf9800
Try
Try
Try
Try
Try
Try
Try
Try
Try
Try
(assuming we can trust finalize()
)