您当前的位置: 首页 >  jvm
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Kotlin (JVM) 语言层面的性能开销 —— object 表达式

沙漠一只雕得儿得儿 发布时间:2021-12-01 11:05:58 ,浏览量:0

内容

相较于Java,Kotlin 提供了委托属性、高阶函数等众多新的语言特性,亦在语言层面提供了丰富的语法糖为开发者提供便利,若对这些语言特性和语法糖背后的原理不甚了解,很容易因误用造成程序错误和性能损耗;此外,我们在使用 Java 时的一些习惯若延续到 Kotlin 等其他编程语言中去时,亦不一定完全适用,使用不当亦很有可能会带来错误或性能问题。为避免产生这样的问题,本文对 Kotlin 语言层面的一些常见的使用细节与 Java 进行了对比探究,以供参考。

  • 本文中总结的这些性能开销 Tips 多数来自于网络上现有的资料,但是会在基于近日发布的新版 Kotlin 1.4 进行重新分析验证
  • Kotlin 是跨平台实现的语言,本文不考虑其在 native、web 等平台的实现,仅仅基于 Kotlin 在 JVM 上的实现探讨其语言特性的一些实现细节
参考
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析 - 掘金
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(二) - 掘金
  • https://medium.com/rsq-technologies/comparative-evaluation-of-selected-constructs-in-java-and-kotlin-part-1-dynamic-metrics-2592820ce80
  • https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
  • https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-2-324a4a50b70
  • https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-3-3bf6e0dbf0a4
  • 探索 Java 隐藏的开销
方法与工具

Intelij Idea 或者 Android Studio 中可以打开 Kotlin ByteCode 工具查看对应字节码,亦可以 Decompile 该字节码以反编译成对应 Java 代码.

我们可以通过对比 Kotlin 和 Java 的同一实现下的差异,对 Kotlin 的一些语言特性的实现、使用时的性能损耗等进行分析学习.

1.单例的实现

在 kotlin 中可以轻松使用 object  声明一个单例,如下:

1

2

3

4

5

6

object Single {

    val hello = "hello world"

    fun sayHello() = "hello world"

}

使用该单例时直接使用类名即可访问对应的成员,

1

2

Single.hello

Single.sayHello()

无论是单例的实现还是使用都比 Java 简洁了许多。实际上其背后的原理也没有多么神奇,将上述单例反编译为 Java 代码,如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public final class Single {

   @NotNull

   private static final String hello;

   public static final Single INSTANCE;

   @NotNull

   public final String getHello() {

      return hello;

   }

   @NotNull

   public final String sayHello() {

      return "hello world";

   }

   private Single() {

   }

   static {

      Single var0 = new Single();

      INSTANCE = var0;

      hello = "hello world";

   }

}

可见其背后的原理仅仅是利用 JVM 的类加载机制实现了一个传统的 Java 饿汉式的单例模式而已。

饿汉式单例的缺点是类一旦加载对应的实例便被初始化,那么如何使用 Kotlin 方便的实现线程安全的、懒汉式的单例模式呢,示例代码如下:

1

2

3

4

5

6

7

8

9

10

class LazySingle private constructor() {

    fun sayHello() = "hello world"

    companion object {

        val INSTANCE by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {

            LazySingle()

        }

    }

}

此处使用了 Kotlin 中提供的委托属性特性,可以参考 Kotlin 的文档,了解 by lazy 的作用。

2.companion object

Kotlin 并不具备 Java 语言的 static 关键字特性,但是可以通过使用 companion object 实现类似的“语法效果”,但 companion object 实现的这种语法效果与 Java Static 特性本质还是有差别的,这其中其实存在一定的性能损耗。

以下代码展示了一个简易的 companion object 示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class OuterClazz {

    private val outerSalary = 12000

    val outerName = "OuterName"

    fun outerSayHi() {

        print("Hi")

    }

    companion object Inner {

        private val innerSalary = 12000

        val innerName = "InnerName"

        fun innerSayHi() {

            print("Hi")

        }

    }

}

在其他代码处访问 innerName 成员时,可以直接使用 OuterClazz.innerName 的形式来访问,语法看上去与 Java 中访问 static 成员类似。

OuterClazz.innerName 这种形式实际上时 OuterClazz.Inner.innerName 的缩写

将上述代码反编译后得到对应 Java 代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

public final class OuterClazz {

   private final int outerSalary = 12000;

   @NotNull

   private final String outerName = "OuterName";

   private static final int innerSalary = 12000;

   @NotNull

   private static final String innerName = "InnerName";

   public static final OuterClazz.Inner Inner = new OuterClazz.Inner((DefaultConstructorMarker)null);

   @NotNull

   public final String getOuterName() {

      return this.outerName;

   }

   public final void outerSayHi() {

      String var1 = "Hi";

      boolean var2 = false;

      System.out.print(var1);

   }

   public static final class Inner {

      @NotNull

      public final String getInnerName() {

         return OuterClazz.innerName;

      }

      public final void innerSayHi() {

         String var1 = "Hi";

         boolean var2 = false;

         System.out.print(var1);

      }

      private Inner() {

      }

      public Inner(DefaultConstructorMarker $constructor_marker) {

         this();

      }

   }

}

外部类实际上实例化了一个 companion object 的单例,我们访问 innerName 的过程实际上是 OuterClazz.Inner.getInnerName, 而 getInnerName 方法又去访问了外部类的一个静态成员. 这其中着实兜了一个大圈子,造成了一定的性能损耗。

可以通过为 Inner 的成员添加 const 或者 @JvmField 声明,避免内部类产生多余的 getXXX 方法、造成多余的访问。

1

2

3

4

5

6

class OuterClazz2 {

    companion object Inner {

        // 此处可以使用 const 或者 @JvmField 将其声明为常量

        @JvmField val innerName = "InnerName"

    }

}

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public final class OuterClazz2 {

   @JvmField

   @NotNull

    // 并未再再 Inner 中生成 getter 方法进行冗余的访问

   public static final String innerName = "InnerName";

   public static final OuterClazz2.Inner Inner = new OuterClazz2.Inner((DefaultConstructorMarker)null);

   public static final class Inner {

      private Inner() {

      }

      // $FF: synthetic method

      public Inner(DefaultConstructorMarker $constructor_marker) {

         this();

      }

   }

}

至此,我们如果再回过头看上一节中使用 companion object 实现的懒汉式的单例,其中实际上也存在比较蠢的一点实现:尽管外部类的实例化延迟了,但是一旦加载外部类,内部也会立即会去初始化为一个 companion object 的实例, 依旧造成了一丢丢内存浪费.

3. 委托属性

前面的 Kotlin 版懒汉式单例使用到了 by lazy 这一 Kotlin 内置的委托属性,并且通过指定 lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) 保证了对实例进行初始化、读取时的线程安全. 若不指定此处的 LazyThreadSafetyMode,那么默认值即为 SYNCHRONIZED,在实际开发时,若是在单线程环境下使用  lazy 委托属性,则可以考虑将 mode = LazyThreadSafetyMode.NONE 来优化性能.

todo: 反编译字节码说明原理. Effective Class Delegation - zsmb.co

关注
打赏
1657159701
查看更多评论
立即登录/注册

微信扫码登录

0.0387s