今天在翻某群历史消息的时候,看到有一篇关于Android的性能优化文章归档的博客,其中有一篇,叫探索Java隐藏的开销。就从字节码和本地代码层面,以及各大App中的情况来探究了语言开销的问题。虽然文章更多的是以安卓的习惯和角度去探寻问题,但很多语言相关及分析思路上,是相通的。写到这里时,我突然又想到了kotlin,kotlin也是java字节码平台上的一种语言,那么它的实现也依赖于并受限于字节码和jvm提供的功能,是不是也会有相同的问题(见第二小点内容,嵌套类的实现)。
第一个小点,讲的是一个Java类中,隐藏(自动创建)的方法。如下代码所示,下面这段代码有多少个方法?
|
|
脑子里回想起学习Java基础的“每个类在没有显示声明构造方法时,都会隐式的有一个空参构造方法”。再想想其他的,好像没有了,遂回答:“一个”。
继续看文章,文章中编译到了dex
文件,使用dexdump
查看信息,打印出函数列表(method_ids_size
)为2,有点出乎意料,猜想是否与继承有关(java的类都继承自Object)。使用javap
查看字节码信息,如下所示:
|
|
在索引 1 处,是我们的对象构造函数,它被父类的构造函数调用。这是因为,即使我们不声明它,
Example
也是继承于Object
的。每一个构造函数都会调用它的父类的构造函数。它是自动插入的。这意味着我们的 class 流中有两个方法。 所有这些关于我的初始问题的答案都是对的。区别就是术语不同。这是真实的情况。我们没有定义任何方法。但是只有人类关心它。作为人类,我们读写这些源文件。我们是唯一关心它们内部构造的人。另外两个方法更重要,方法的个数实际上是编译进 class 文件里面了。无论是否声明,这些方法都在 class 的内部。
上面这段是原文章的回答,目前我还不是很能理解第二个函数关于父类构造函数。我对题目的理解是该Example
类包含定义的方法,而父类构造方法是调用的部分,不知道为啥这里也被包含了进去。和同事讨论后,他的理解是,父类的方法是放在了子类的里面。所以回到了Java这里,继承到底是怎么实现的问题。之前也和他有凭感觉的猜想过,得出的结论是类似于大圆包含小圆,子类包含了父类。今天回想起来,感觉好像不是很对,于是手动尝试上面的实验。测试如下:
|
|
这里编写了3个类,Example、B和C,其中Example继承了B,B继承了C,除了因为需要打jar包的缘故,给Example类写了一个main方法外,都是没有写其他方法的。使用文章中提到的工具dex-method-list
,打印出详细信息,如下:
|
|
可以看到,总共有5个方法,分别是Example类、B类和C类的构造方法,main 方法和一个Object的构造方法。到这里,可以得出,之前和同事猜想的结论是错误的,属于瞎猜。然后这里主要的疑问点,其实是dexdump
工具这里的method_ids_size
计数,到底是指哪一部分的问题。所以又写了如下来进行测试:
|
|
所以dexdump
方法统计的应该是所有包含的方法的数量,类似于方法表一样。另一方面,子类初始化时,不会实例化父类。说到初始化,这里简单回顾一下:
首先Java的类通过new关键字和构造方法来实例化,这里构造方法其实应该翻译为构造器(constructor
)比较好,因为它实际上和Java里的方法并不一样。
1、在概念层面,方法属于Java的类成员,而构造器不属于。构造器和方法是平等的概念,而不是包含(Filed:成员变量、Method:普通方法、Type:内部定义的其他类型,如内部接口、内部类等),
2、在虚拟机层面构造器使用invokespecial
指令执行,而方法使用invokevirtual
指令执行。
Constructors, static initializers, and instance initializers are not members and therefore are not inherited. ---- 8.2 Class Memebers章节 构造方法,静态初始化器,对象初始化器,都不是类的成员,因此它们也不可以被子类继承。
a constructor declaration looks just like a method declaration that has no result... ---- 8.8 Constructor Declarations 章节 一个 constructor 的定义就像是一个没有返回值的 method 的定义。
说到这里,有一些相关的题目:“Java的构造方法到底有还是没有返回值?”、“Java中的构造方法是方法吗?”。现在这些问题大家心里就有自己的见解了吧。
然后
上面说的Java关于new一个对象的侧重点是由这篇文章衍生的部分,是围绕文章相关来的,所以其中还有更多的细节,例如对象的访问定位问题、类加载过程等,大家可以自己去更深入的了解学习。相信下面这类题目,已经完全没有难度了:
|
|
在上面查找资料的过程中,还有另一个有意思的问题。在查看字节码时,发现在构造方法中有一个字节码指令aload_0
,不知道是做什么的,于是就查了一下百度,看到这样一条信息:
总结起来:在非静态方法中, aload_0 表示对this的操作,在static 方法中,aload_0表示对方法的第一参数的操作。
尝试编写如下代码,并编译到字节码查看:
|
|
这里总共写了3个方法,前两个是静态方法,第三个是非静态方法。参数上面,分别使用了基本类型和引用类型进行对照。
现在来看字节码的情况,首先方法f1
中,果然出现了aload_0
代表第一个参数,但是在方法f2
中,就不是aload_0
了,而是iload_0
,所以aload_0
应该是加载第一个引用类型的变量。然后继续看非静态方法f3
,可以看到,虽然在Java代码中它的第一个参数是基本类型,但是第一行指令任然是aload_0
,而第一个参数int
类型使用的指令却是iload_1
,不是从0开始,而是从1开始了。所以结论就很明显了,*load_*是加载指令,其中load前面的代表变量的类型,而load后面的代表参数位置,但是非静态方法比较特殊。这个时候,要是以前的我呀,多半会去死记硬背了:”非静态方法的第一个参数是从1开始“。
但是,恰好最近学了Rust,又恰好,其中有一部分概念比较相似,所以我们可以用更加理解的方式,去学习。看如下代码:
|
|
把struct A
当作Java中的类,main
方法中,let a = A();
就相当于Java中new
一个实例对象出来。
回到文章,第二部分讲的是关于java嵌套类的隐藏开销,如下代码:
|
|