最近在做开发的作业中,意外发现了kotlin官方招认的一个内联类的bug。在了解这个bug产生的原因的进程中,我秉承着打破砂锅问毕竟的决心,竟然顺势学习了一波jvm字节码。收成颇java作业培训班丰,所以便开始着手写下这篇文java面试题章和java作业培训班咱们同享一下这个学习的进jetbrains官网程。这篇文章很长,可是耐性看完,我信任咱们肯定会觉得很值。

传闻inline class很屌

作业是这样的。团队的领头大哥上星期给我安利了一波kotlin的内联类,说这玩意好用的很,节省内存。所以顺手写了一个sample给我看看。还没了解过内联类(inline class)的能够看看官方文档

有时分,事务逻辑需求环绕某种类型创立包装器。可是,由于额定的堆内存分配问题,它会引入作业时的功用开支。此外,假定被包装的类jetbrains中文版型是原生类型,功用的丢掉是很糟糕的,二进制转八进制由于原jetbrains中文版生类型通常在作业时就进行了很多优化,可是他们的包装器却没有得到任何特别的处理。

简略来说,便是比方我定义了一个Password类

class Password{
private String password;
public Password(String p)java言语{
this.password = p
}
}

这种数据包装类功率很低,而且占内存。由于二进制亡者列车这个类实际上只包装了一个String的数据,可是由于他是一个单独声明的类,所以假定new Password()的话还需求单独给这java怎样读个类创立一个实例java难学吗,放在jvm的heap 内存里。

假定有一种方法,既能够让这个数据类坚持它单独的类型,又不那么占空间,那岂不是完美?用inline class便是一个很好的挑jvm参数选。

inline class Password(val value: Stri接口类型ng)
// 不存在 'Password' 类的实在实例二进制手表政策
// 在作业时,'securePassw接口crc过错计数ord' 仅仅包括 'String'
val securePassword = Password("Don'二进制t try thijvm是什么意思s in production")

Kotlin会在编译的时分检查inline class的类型,jvm参数可是在作业时runtime仅仅包括String数据。(至于它为啥这jetbrains官网下载么屌,下面会经过字节码接口crc过错计数剖析)

那已然这个类这么好用,我就开始试试了。

inline class的jetbrains什么意思

俗话说得好,试试就逝世。没多久我就发javascript现一个很奇葩的现象。示例代码如下

我先定义了一个inline class

inline class ICAny construc二进制怎样算tor(val value: Any)

这个类仅仅是一个包装类,包装一个恣意类型的value(在jvm里边便是Object)

interface A {
funjetbrains怎样读 foo(): Any
}

一起定义一个interface, foo方法回来恣意类型。

class B :jetbrains是什么软件 A {
override fun foo(): ICAny {
return ICAny(1)
}
}

接着完毕这个interface,在重载的f接口文档oo的回来值上面咱们回来刚刚定义的inline class类。由于ICAny肯定是Any(在jvm里边是Object)的子类,所以这个方法是能够经过编译的。

接下来共同的作业产生了。

在调用下面的代码的时分

 fun test(){
val foo2: Any = (B() as A).foo()Java
println(foo2 is ICAny)
}

打印效果竟二进制计算器然是False

也便是说,foo2这个变量,不是ICAny类。

这就很共同了,class B的foo现已是明确的回来一个ICAny的实例了,哪怕我做一个向上转型,也不应该影响foo2这个变量在作业时的类型啊。

字节码有问题么?

虽然我不太懂字节码,可是我的直觉告诉我应该趁便看一眼,所以我便顺手运用Intelji的kotlin字节码功用,打开了这段代码的字节码。

Kotlin 内联类 inline class请了解一下

一看,好家伙,除了injvm废物回收机制stanceOf这个方法需求判别ICAny类之外jetbrains clion,没有一段字节码和ICAny类有关。

Kotlin 内联类 inline class请了解一下

我的直觉是,已然B类的foo方法回来的是ICAny类实例,那调用这个方法的代码块怎样也得有一个变量是这个ICAny类吧。效果是接口的效果编译好的字节码竟然完全没有ICAny类什么事。着实古怪。

字节码入门

为了完全搞理解这毕竟是为啥。我决议jvm优化要开始入门一些字节码的常识。。。。网上关于字节码的资料许多,这儿我就只同享一下和咱们这次bug有关的常识二进制转八进制

首要字节码看起来有点像学过的汇编言语相同,比二进制要简略懂,可是又比高档言语不流通一些,而且都是用有限的指令集完毕高档言语功用。毕竟,最重要的一点,大部分JVM都是用栈来完毕字节码的。咱们接下来用比方详细了解一下这个栈毕竟是啥。

class Test {
fun test(){
val a = 1;
vjetbrains是什么软件al b = 1;
val c = a + b
}
}

比方上面这个简略的test方法,变成字节码之后,长这个姿势

public final test()V
L0
LINENUMBER 3 L0
ICONST_1
ISTORE 1
L1
LINENUMBER 4 L1
ICONST_1
ISTORE 2
L2
LINENUMBER 5 L2
ILOAD 1
ILOAD 2
IADD
ISTORE 3
L3
LINENUMBER 6 L3
RETURN
L4
LOjetbrains官网C二进制手表ALVARIABLE c I L3 L4 3
LOCALVARIABLE b I L2 L4 2
LOCALVARIABLE a I L1 L4jvm废物回收机制 1
LOCALVARIABLE this LTest; L0 L4 0
MAXSTACK = 2
MAXLOCALS = 4

看起来如同很凌乱,其实十分简略了解,咱们一个指令一个指令的看。详细哪个指令是干什么的,咱们参照这个JVM指令jvm是什么意思集表格 enjava言语.wikipedia.org/wiki/Java_b…

榜首步L0

Kotlin 内联类 inline class请了解一下

ICONST_1,在字节码里边定义接口和抽象类的差异

load the int value 1 onto the stack

那么当时的栈帧就有了榜首个数据,1

Kotlin 内联类 inline class请了解一下

第二步是 ISTORE 1 ,在字节码里边ISTORE的定义java开发

stjava难学吗ore int value into variable #index, It is popped from the operand stack, and the二进制转八进制 value of the二进制手表 local variable at index isjvm内存模型 set to value.

意思便是这个操作会把栈中的顶端数字pop出来,然后赋予index为1的变量。那二进制怎样算index为1的变量是哪个变量?字节码的第四部分现已给jetbrains什么意思出了答案。便是变量 a

Kotlin 内联类 inline class请了解一下

一起,由于ISTORE会pop栈顶数字,此刻栈变空了。

Kotlin 内联类 inline class请了解一下

字节码的第二部分和榜首部分几乎一模相同,仅仅赋值变量从a变成了b(留心ISTORE的参数是2,对应index为2的变量,便是b)

 L1
LINENUMBER 4 L1
ICONST_1
ISTORE 2

字节码第三部分

 L2接口
LINENUMBER 5 Ljvm原理2
ILOAD 1
ILOAD 2
IADD
ISTORE 3

榜首二个指令是ILOAD,定义jetbrains中文版

load an int value from ajetbrains什么意思 local variable #index, The value o接口测验f the local variable at index is pushed onto the ope二进制转十进制计算器rand stack.

也便是说jetbrains idea,这个指令会获取index为1和2的变量的值,而且把值放入栈顶。

那么经过ILOAD 1和ILOAD 2之后,栈内元素变成了

Kotlin 内联类 inline class请了解一下

第三个指令是IADD

add two ints, The values are popped from the operand stack. The int rjetbrains官网esult is value1 + value2. The result is pushed onto the operand stack.

也便是说,这个指令会把栈顶的两个元素别离pop出来而且相加,相加的和再放入栈中,也便是说此刻栈内元素变成了

Kotlin 内联类 inline class请了解一下

毕竟一步

ISTORE 3

也便是把栈顶元素赋值给index为3的变量,也便是c, 毕竟,c被赋值为2.

以上,便是字节码的根底,它以栈为容器,处理每个指令的回来值(也或许没有回来值)。一起,JVM的大部分指令,都是从栈顶获取参数作javaee为输入。这个接口crc过错计数规划,使得JVM能够在单个栈里边处理一个方法的作业。

为了能让咱们更深化的了解这个栈的运用方法,我这儿留一jvm优化个小作业。了解了这个小作业的原理,咱java面试题再继java难学吗续往下看。不然就多研究一下。有必要完全了解透彻JVM中栈的运用方法才行。

作业

一个简略的代码

 fun test(){
val a = Object()
}

字节码为

  LINENjvm内存结构UMBER 3 L0
NEW java/lang/Object
DUP
INVOKEjvm参数SPECIAL java/lang/Object.<init> ()V
ASTORE 1

请问为什么在履行完NEW指令之后,需求运用D接口卡UP来仿制刚刚NEW出来政策的reference到栈顶

inline class的字节码?

在学习完字节码根底java难学吗之后,我就开始揣摩一下,是不是该研究一下inline class的字节码和一般类的字节码有啥不同。

公然,在得到inline class的Java字节码java开发之后,共同的东西呈现了。

以下面这个inline class为比方

inline class ICAny(val a: Any)

字节码中,差异于一般的类,这个i二进制转化为十进制nline class的结构函数jvm废物回收机制符号为了private,也便是外部代码不能运用inline class的结构函数。

Kotlin 内联类 inline class请了解一下

可是在代码中运用

    v接口al a = ICAny(1接口和抽象类的差异)

却是没接口的效果有过错的。很共同。。。。

第二,inlinjetbrains clione class多了一个叫constructor-impl的方法,看姓名和结构函数有关,可是仔细看,这个方法啥也没干,便是用ALOAD把输入的参数读取到栈之后,又马上弹出回来了.(留心该方法的输入类型是Object)

Kotlin 内联类 inline class请了解一下

带着许多疑问,咱们来看看当咱们创立一个inline class实例的时分,编译器毕竟做了二进制亡者列车啥。

 val a = ICAny(1)

上面这段kotlin代码对应的字节码jvm原理是:

 L0
LINENUMBER 6 L0
I二进制转八进制CONST_1
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang接口是什么/Integer;
INVOKEjvm优化STATIC com/jetbrjetbrains怎样读ains/handson/mpp/myapplication/ICAny.constructor-impl (Ljav二进制转八进制a/lang/Object;)Ljava/jvm原理lang/Object;
ASTORE 1

共同的当地就在于,这段字节码完全没接口卡有履行过NEW指令。

NEW指令是用来分配内存的。NEW之后协作init(结构函数)能够完毕一个政策的初始化。

比方咱们创立一个HashMap:

 val a = HashMap<String,String>()

对应的字节码是:

L0
LINENUMBER 10 L0
NEW java/util/HashMap
DUP
INjetbrains激活码VOKESPECIAL java/util/Ha接口文档shMap.<init> ()V
ASTORE 1

能够很明显的看jetbrains中文版出来,字节码先履行N二进制亡者列车EW指令,划分了内存。然后再履行了HashMap的结构函数init。这是一个创立政策的规范流程,很可惜的是从inline class的创立进程中咱们完全看不到这个进程。也便是说,当咱们写出jetbrains idea代码:

val a = ICAny(1)

的时分,JVM压根都不会拓荒新的堆内存。这也说明晰为啥inline class在内存上有优势,由于它仅仅从编译的角度把值给包装起来,不会创立类实例。

可是假定压根都不创立类实例,那假定咱们做instjetbrains中文版anceOf的操作,接口的效果岂不是不能正常作业?JVM

fun test(){
val a = ICAny(1)
if( a is ICAny){
print("ok")
}
}

这段代码的字节码编译出来的字节码会被JV接口英文M优化,JVM编接口译器根据上下文判别a肯定是ICAny类,所以在字java怎样读节码中你甚至都看不到有if的呈现javascript,由于编译器优化之后会发现if一定是true。

Kotlin 内联类 inline class请了解一下

inlijetbrains中文版ne class的装箱拆箱

带着疑问,我开始检查inline class的规划文档。走运的是,jetbrian对这些规划文档都是公开的。在规划文档 中,jjetbrains clionetbrian的工程师详细的说明晰关于inline class的类型问题。

原文是这样描绘的

Ru接口类型les for boxing are pretty the same接口卡 as二进制转化为十进制 for primitive types and cajava初学n be formulated as follows: inline class is boxed when it is used as another type. Unboxedjava模拟器 inline class is used when value is statically known to be inline class.

大约意思便是inline class也需求装箱拆箱,就和Integer类和int类型相同。在有需求的时分编译器会对这两种类型做转化,转化的进程便是装箱/拆箱。

那关于inline class来说,什么时分需求拆箱,什么时分需求装箱呢?上文现已给出了回答:

inline class is boxed when it is used as another type

当inline class在rujava难学吗ntime的时分被当成另一种类型运用的时分,就会装箱。

Unjava初学boxed inline class is used when value is statically known to be inline class

当inline class 在静态剖析中被认为是作为inline class本身履行的时分,就不需求装箱。

或许这样说有点绕口,咱们用一个简略的比方来说明:

 fun test(){
val a = ICAny(1)
if( a is ICAny){
print("ok")
}
}

上面这段代码中,JVM编译器在编二进制计算器译阶段就能够经过上下文的静态剖析得出a一定是ICAny类,这种状况就符合unbox的jvm调优东西条件。由于编译器在静态剖析阶段就现已获取了类型信息,咱们就能够运用拆箱的inline class,也便是字节码不会生成一个新的ICAny实例。这样也符合咱们之前剖析。

可是假定咱们修改一下运用方法:

    fun test() {
val a = ICAny(1javascript)
bar(a)
}
private funJava bar(a: Any) {
if (a is ICAny) {
print("ok")jvm调优
}
}

加入了一个叫bar的方法,该方法的输入是Any,也便是JVM中的Object类。这段代码编译出来的字节码,就需jetbrains官网下载求装箱操作了

Kotlin 内联类 inline class请了解一下

ICAny的装箱操作方法,和java面试题prjava言语imit接口是什么ive type相似,其实便是履行NEW指令,创立一个新的类实例

Kotlin 内联类 inline class请了解一下

总结一下,当运用inline class的时分,jvm调优假定当时代码根据上下文能够揣度出变量一定是inline class类型,编译器就能够优化代码,不生成新的类实例,然后达到节省内存空间的目的。可是假定经过上jetbrains idea下文揣度不出来变量是否是inline class,编译器就会调javaee用装箱方法,创立新的inline class二进制类实例,划分内存空间给inline class实例,也就达不到所谓的节省内存的目的了。

官方给出的比方如下

Kotlin 内联类 inline class请了解一下

其间值得留心的是泛型也会让inline clasjetbrains-agent.jar怎样用s产生二进制八进制十进制十六进制转化装箱,由于泛型其实和kotlin的Any是相同的性质,在JVM字节码中都是Object。

这也给咱们提了个醒,假定你的代码不能经过上下jetbrains中文版文判别inline class类型,那运用inline class或许并没啥卵用。。。。java模拟器

inline class的bug是什么原因产生的

在了解完根底常识之后,咱们总算能够开始了解为什么在文章开始时分说到的bug会产生了。Kotlin官方现已意识到这个bug而且把bug产生的原因详细说明晰一jvm调优东西下: youtrack.jjetbrains ideaetbrains.com/issue接口/KT-30… (在这儿十分赏识jetbrian的工程师的风格,能够说是写的十分详细了接口英文)。

这儿稍微说明一下给看不太懂英文的小伙伴:

在JVM中,kotlin和java都是二进制转化为十进制支撑多态/协变的。比方jvm原理在下面这个承继联络中:

interface A {
fun foo(): Any
}
class B : A {
override fun foo(): String { // Covariant override, return type is more specialized than in the parent
return ""
}
}

这样的代码编译是完全ok的,由于ICAny能够看做是承继了Object类,所以Class B作为承继A接口的实体类,重写的方法的回来值能够是和接口类方法的回来值呈承继联络的.

在class B的字节二进制码中,编译器会生成一个jvm内存模型接办二进制(bridge method)来让重写的foo方法回来String类,可是一起方法签名坚持父类的类型。

Kotlin 内联类 inline class请了解一下

JVM正是依靠着桥接方法,完毕了承继联络的协变。

可是到了inline class这儿,就出大问题了。关于inline cla接口测验的流程和步骤ss来说,由于编译器会jvm内存结构默许将其作为Object类型,会导致某些实体类无法生成桥接方法的bug。

比方:

interface A {
fun foo(): Any
}
class B : Ajetbrains激活码 {
override fun foo(): ICAny {
return ICAny(4)
}
}

由于ICAny类在JVM中是Object类型,Any也是Object类型,编译器就会主动认为重写方法的回来值是和interface相同,所以不会生成ICAny的桥接方法。

Kotlin 内联类 inline class请了解一下

所以回到咱们文章最初的bug代码中

       val foo2: A接口的效果ny = (接口是什么B() as A).foo()
println(foo2 is ICAny)

由于B没有ICAny类型的桥接方法,接口加上在代码中咱们强制转型把B转成了Ajvm性能调优类,所以静态剖析也会认为foo()方法的回来值是Any,就会导致foo2变量不会被装箱,所以类型就一直是Objejava面试题ct,以上代码jetbrains官网下载的打印效果也便是False了。

Kotlin 内联类 inline class请了解一下

所以相应的,处理这个bug的方法也很简略,便是给inline cljavaeea二进制转化器ss加上桥接方法就好了!

这个bug在kotlin 1.3的时分被发现,在1.4被fix。可是鉴于二进制转化为十进制大部分安卓使用开发还在运用1.3,这个坑或许还会长期存在。

升级到kotlin1.5之后jvm参数,打开字节码东西能够发现桥接方法现已java难学吗被加上啦:

Kotlin 内联类 inline class请了解一下

总结

在了解这个bug的原因和处理方法的进程中,我开始查验了解字节码,一起学习JVM的调用栈,毕竟拓宽到字节码对协变多态JetBrains的支撑,能够说收成真的许多。期望这个学习方法和进程能够给更多的朋友一些启示,当咱们遇到问题的时分,需求做到知其然,还要知其所以然,这么多年的阅历告诉我,掌握好一门学科的根底是jetbrains什么意思能够让jvm是什么意思之后的作业事半功倍的。与咱们共勉!