从val跟var了解虚拟机世界
val 跟 var
val本意就是一个不可变的变量,即赋初始值后不可改变,想较于val,var其实就简单的多,就是可变变量。为什么说val是不可变的变量呢?这不就是矛盾了嘛,其实不矛盾,我们在字节码的角度出发,比如有
val a = Test()
var b = Test()
变成的字节码是
private final Lcom/example/newtestproject/Test; a
private Lcom/example/newtestproject/Test; b
其实val 本质就是用final修饰的变量罢了,而var,就是一个很普通的变量。两者默认都赋予private作用域,这个其实是kotlin世界赋予的额外操作,并不影响我们的理解。从这里出发,我们再继续深入进去!
一个有趣的实验
companion object{
val c = Test()
const val d = "1"
const val e = "1"
val r = "1"
val v = d
}
如果我们把val变量放在companion object里面,这个时候就会被赋予静态的特性,我们看下上面这段代码生成后的字节码
private final static Lcom/example/newtestproject/Test; c
public final static Ljava/lang/String; d = "1"
public final static Ljava/lang/String; e = "1"
private final static Ljava/lang/String; r
private final static Ljava/lang/String; v
我们可以看到,无论是普通对象还是基本数据类型,都被赋予了static的前缀,但是又有稍微不同??我们再来仔细观察一下。
对于String类型,可以用const关键字进行修饰,表示当前的String可用于字符串常量进行替换,这个就是完全的替换,直接进行了初始化!而没有const修饰的字符串r,可以看到,只是生成了一个r变量,并没有直接初始化。而r被初始化的阶段,是在clinit阶段
static void <clinit>() {
ldc "1"
putstatic 'com/example/newtestproject/ValClass.r','Ljava/lang/String;'
...
假如说我们用java代码去写的话,比如
public class JavaStaticClass {
static final String s = "123";
...
}
所生成的字节码是
final static Ljava/lang/String; s = "123"
跟我们kotlin用const修饰的string变量一致,都是直接初始化的!(留到后面解释)我们继续深入一点,为什么有的变量直接就初始化了,有的却在clinit阶段被初始化?那就要从我们的类加载过程说起了!
类加载过程
虽然类加载有很多细分版本,但是这里笔者引用以下细分版本
由于类加载过程不是本篇的重点,这里我们稍微解释一下各阶段的主要任务即可
- 加载:载入类的过程 :主要是把类的二进制文件,转化为运行时内存的数据,包括静态的存储结构转为方法区等操作,在内存中生成一个代表这个类的java.lang.Class对象
- 验证:验证class文件等是否合法:确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
- 准备:准备初始数据 :准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
- 解析:解析常量池,函数符号等 :解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,这个阶段就把我们普通的符号转化为对内存运行数据地址。
- 初始化:真正的初始化,调用clinit:在初始化阶段,则会根据代码去初始化类变量和其他资源,这个时候,就走到了我们clinit阶段了,上面的阶段都是由虚拟机操控,这个阶段过去后就正在把控制权给我们程序了
准备阶段对static数据的影响
我们主要看到准备阶段:准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,即在这个阶段过后,所有的static数据被赋予“零值”,以下是零值表
但是也有例外,就是如果类的属性表中存在ConstantValue这个特殊的属性值时,就会在准备阶段把真正的常量直接替换给当前的static变量,比如上述代码中的
省略companion object
const val d = "1"
public final static Ljava/lang/String; d = "1"
此时,只要对d的操作,就会被转化为以下字节码,比如
val v = d
字节码是
ldc "1"
putstatic 'com/example/newtestproject/ValClass.v','Ljava/lang/String;'
变成了ldc指令,即押入了一个字符串“1”进了操作数栈上,而原本的d变量盒子,已经彻底被虚拟机抛弃了。对于属性表中没有ConstantValue的变量,就会在初始化阶段,即调用clinti时,就会把数值赋给相关的变量,以替换“零值”(ps:这里就是各大字节码精简方案的核心,即删除把零值赋予零值的相关操作,比如static int xx = 0这种,就可以在Clint阶段把相关的赋值字节码删除掉也不影响其原本数值,参考框架bytex)。
当然,我们看到上面的对象c,也是在clinit阶段被赋值的,这其实就是ConstantValue生成机制的限制,ConstantValue只会对String跟基本数据类型进行生成,因为我们要替换的常量在常量池里面!对象肯定是不存在的对不对!
回归主题
看到这里,我们再回来看上面的问题,我们就知道了,kotlin中companion object里面的字符串变量,如果不用const修饰的话,其实对应的字符串String类型是不会以ConstantValue生成的,而是以静态对象相同的方式,在clinit进行!
说了半天!那么这个又有什么用呢!?其实这里主要是为了说明虚拟机背后生成的原理,同时也是为了提醒!如果以后有做指令优化的需求的时候,就要非常小心kotlin companion object里面的非const 修饰的String变量,我们就不能在Clinit的时候把这个赋值指令给清除掉!或者说不能跳过Clinit阶段就去用这个数值,因为它还是处于未初始化的状态!
最后
我们从val跟var的角度出发,分析了其背后隐含的故事,当然,看完之后你肯定就彻底懂得了这部分知识啦!无论是以后字节码插桩还是面试,相信可以很从容面对啦!
笔者说:如果你看过这篇文章 黑科技!让Native Crash 与ANR无处发泄!,就会了解到Signal的今生前世,同时我们也发布了beta版本到maven啦!快来用起来!
作者:Pika
链接:https://juejin.cn/post/7125593351264403464
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。