代码之家  ›  专栏  ›  技术社区  ›  Tom

为什么在Kotlin JVM中强制GC扫描时,一些垃圾没有被收集,这取决于看似无关的因素?

  •  0
  • Tom  · 技术社区  · 4 年前

    上下文

    我正在开发一个Kotlin程序,该程序在JVM上运行并消耗大量内存。虽然我确实相信垃圾收集器会(最终)释放不再可访问的对象所使用的内存,但我不相信项目的未来维护者(包括我未来的自己),尤其是随着项目的进展和发展——以确保不再需要的对象确实不可访问的方式编写代码。

    因此,为了降低风险,作为我的测试套件的一部分(该套件在程序功能的逻辑方面已经详尽无遗),我还在编写(或试图编写,并取得不同程度的成功)各种测试,旨在确保不会保留对运行过其程序的对象的引用。

    由于这很难直接完成,我在测试中使用的技术是使用带有终结器的对象,在不再需要它们时模拟条件,强制垃圾收集,并断言终结器确实已经运行。(注意:我的问题不是关于这项技术本身,但如果有人有改进的意见或想法,或者可以提出替代方案,我会很感兴趣的!)。

    这通常工作得很好,并且可以被证明可以完成这项工作,例如在TDD风格中:我编写简单的代码,它可以完成业务逻辑方面的工作,但不考虑丢失对旧对象的引用,我如上所述编写测试,我确保测试失败,我添加代码来处理内存(例如,在简单的情况下,将引用设置为 null ),然后查看测试是否通过。

    我的问题

    出于某种原因,我的测试并不总是有效(澄清:我的意思不是说它们非决定性地失败;我的意思是有些测试一直有效,有些测试一直失败)。该项目的真实例子包含了许多复杂的专有细节,但我已经设法将其归结为以下最小的例子:

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

    上面写的测试通过了,但如果不是调用 f 我打电话 println 直接失败(在最后一次断言时,即终结器尚未运行)!我的问题是为什么会这样,和/或应该做些什么来确保(垃圾被收集和)终结器确实运行。

    (注意:这不是关于使用 println 或者调用我自己的函数;我的真实代码要复杂得多,并且调用了自己的函数,与打印无关。这只是我设法想出的一个最小的例子,它显示了不一致性。)

    如果重要的话,我在Java 11.0.6上使用Kotlin 1.5.10(在macOS上)。

    更新:

    我把这两个测试并列编写(在文件中 T.kt 包装内 t );下面请找到源代码,以及反汇编(使用 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
    }
    
    0 回复  |  直到 4 年前
        1
  •  3
  •   Holger    4 年前

    看来,Kotlins println(…) 函数的行为与Java不同 System.out.println(…) 声明,关于评估顺序。

    在Java中编写

    System.out.println(new Object() { {
        System.setOut(new PrintStream(OutputStream.nullOutputStream()));
    }});
    

    该消息仍将被打印,因为方法接收器, System.out 先计算,即读取字段,然后再计算参数表达式,即创建内部类对象并执行其构造函数。所以代码会改变 System.out 首先在使用该值进行打印之前, System.out 在更改之前。

    因此,字节码看起来像

      0: getstatic     #35                 // Field java/lang/System.out:Ljava/io/PrintStream;
      3: new           #41                 // class my/test/EvaluationOrder$1
      6: dup
      7: invokespecial #43                 // Method my/test/EvaluationOrder$1."<init>":()V
     10: invokevirtual #44                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    

    第一条指令推送 System.out 则更多涉及的对象实例化序列跟随, new 创建并推送新的未初始化对象, dup 复制引用,如下 invokespecial 使用一个并执行构造函数。

    之后,堆栈包含的旧值 System.out ,后跟对初始化对象的引用,适用于调用 println 方法

    Kotlin生成的字节码不同:

      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
    

    当构造函数调用接收到另一个新构建的对象(指令12-20)时,您的用例会更加复杂,但我们可以忽略这一点,而专注于其他方面。

    引用的第一条指令(第8条)是在阅读 System.out ,作为 print(…) 函数,已完成 之后 ,在指令29中。

    按照要求的顺序提供这些参考资料 invokevirtual … println ,即[ PrintStream ,argument object],生成的代码引入一个合成变量来临时保存对构造对象的引用。指令26, astore_2 ,将对构造对象的引用存储到变量#2中。然后,如上所述,指令29推送 System.out 以及指令32, aload_2 从变量#2加载对对象的引用,这将导致需要调用的堆栈内容 println 在指令33中。

    只抓了一个。现在,变量#2包含对对象的引用,字节码既没有作用域,也没有变量删除操作。引用将保留在局部变量中,直到该变量被覆盖用于其他目的或方法返回为止。当我们浏览剩余的代码时,我们看到在这个特定的设置中,变量#2不会再次使用,因此,当代码调用 System.gc() ,引用仍保留在合成变量中,可能会阻止垃圾收集。

    如中所述 Can java finalize an object when it is still in scope? 被本地变量引用并不能阻止垃圾回收本身。从形式上讲,当对象没有被使用超过该点时,它仍然有资格进行垃圾回收。但在实践中是否会收集垃圾,取决于方法的优化状态。因此,当这段代码在您的场景中可重复地防止垃圾收集时,就不足为奇了。

    如果Kotlin支持使用 System.out.println() 具有与Java相同的行为,而不是其内置 打印() 函数,它应该改变行为。

    无论出于何种原因,指令27和28都将常数0存储在从未使用过的变量#3中。这不会影响其余代码的行为。

        2
  •  0
  •   Tom    4 年前

    (Meta:回答我自己的问题,不是为了减少公认的答案,而是为了添加一些信息,这些信息可能对那些碰巧遇到类似问题并最终阅读本文的人有用。)

    原因

    Kotlin编译器似乎有时会在引擎盖下引入局部变量来存储函数使用的各种值,原因对开发人员来说可能并不明显(事实上,我不知道它为什么这么做;如果有人知道添加答案或注释会很酷的话)。一旦从局部变量引用了一个对象,尽管理论上在最后一次使用该变量后仍然可以对其进行垃圾收集,但在实践中似乎没有(至少在某些设置中,可能不是所有设置都看到 this question 例如)。

    诊断

    诊断这种情况的一种方法是查看生成的Java字节码,例如使用 javap -c ,或自己的IDE 显示字节码 选项(在Intellij IDEA上) Tools -> Kotlin -> Show Kotlin Bytecode )。 注: 确保查看实际的字节码,或者至少查看与项目设置一致的字节码。例如,默认情况下,Intellij当前显示通过传统Kotlin编译器生成的Kotlin字节码,而不是使用Kotlin IR的较新编译器,在某些情况下,这与正在运行的实际代码不同,后者是通过Kotlin IR编译的(请参阅 issue )。

    字节码将显示 store 局部变量的指令,包括未在代码中明确定义但由编译器添加的此类变量。

    例如,在问题中发布的字节码的以下摘录中:

      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
    

    人们可以看到 FinalizationNotifier 正在初始化的实例开始于 8: 并在之前完成 26: ,然后 26: astore_2 将其保存在局部变量中(编号2;局部变量编号0是 this 例如,这里的第一个是 var numTimesHasBeenDisposed = 0 ,从开始初始化 0: 并存储在 7: )。

    什么时候发生的?

    如上所述,我真的不知道Kotlin编译器为什么会这样做。我也远不知道发生这种情况时的所有情况。但我确实注意到了两个不同的情况(一个来自问题中的最小示例,另一个来自我的实际用例):

    将表达式传递给时 println : 这就是问题中的示例中发生的情况。调用时 System.out.println 直接地,生成的字节码首先得到 System.out 到堆栈,然后计算传递给它的表达式,将其留在堆栈上的下一个,然后调用 println 方法但打电话给Kotlin’s println ,是传递给它的表达式首先进行计算,然后才进行计算,就好像 println 调用自身字节码 System.out 。在不将表达式的值存储在局部变量中的情况下,这两个元素在堆栈中的顺序与实际调用所需的顺序相反。Kotlin解决这一问题的一种方法可能是保留原始顺序(理论上这可能会对语义产生一些影响,尽管我几乎看不出这在实践中会有什么不同)。另一个是 swap 堆栈上的值。但出于某种原因,Kotlin使用了一个局部变量。

    构造阵列时: 这就是在我的实际用例中发生的情况。我正在将一个特别构建的数组传递给一个函数(通过 vararg 事实上,但在尝试了几个案例后,似乎没有什么不同)。当不是通过Kotlin IR编译时,数组是在堆栈上构建的,但当编译Kotlin IR时,生成的字节码使用本地变量来存储数组。

    如果有人在类似的情况下读到它有类似的问题,这可能是原因。可能还有其他情况。任何知道的人都欢迎回答/评论/编辑。当然,您可以通过查看上面解释的字节码来诊断问题,无论您的案例是否在这里列出。

    救济

    我设法想出了三种不同的方法来应对这种情况:

    • 在使用的特定情况下 println ,可以简单地打电话 System.out.println 直接地

    • 更一般地说,可以将创建附加变量的代码包装在另一个函数中(该函数可以但不一定是嵌套函数)。这样,当包装函数的执行结束时,创建的局部变量将不再可访问,并且它们将被垃圾收集。(这是通过原题的一个测试。)

    • 另一种选择是显式地将值分配给一个变量,而不是让Kotlin在后台隐式地这样做,然后将其他东西分配给该变量(必须是 var ,不是 val )或者,如果该值仅用作引用我们稍后要垃圾收集的实际对象的容器(例如,如果它是一个可以清除它的数组),则更改该值。(也可以使变量为null并赋值 null 但是,根据用例的具体情况,有时可能需要使用一些机制,例如 !! 运算符将值转换为不可为null的类型,而这又是一个新的表达式,最终可能会被分配给隐式局部变量,从而破坏整个转换的目的。)