开篇就提到效能优化涉及的范围会很广,考虑后面须要常常用到 asm 字节码插桩,咱们首先从 《Gradle 插件 + ASM 实战》开始讲,但又但愿你们能知其然也知其因此然,所以咱们首先得讲下 JVM 虚拟机加载 Class 字节码的原理。这每每也是我面试新同窗必问的一个内容,由于若是对这个不了解的话,像插件化与热修复、性能优化、覆盖率统计等等不少功能都是很差实现的。小公司不多有人用,这也是实话,至于你们要不要学,这就看我的状况了,其实也不是用不用得上的问题,就看你们愿不肯意作一个吃螃蟹的人。咱们主要从如下三个方面来讲:java
咱们先来看一个很是简单的 HelloWorld.java面试
public class HelloWorld {
public HelloWorld() {
}
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
复制代码
用文本编辑器打开生成的 HelloWorld.class 文件,是这样的:数据库
cafe babe 0000 0033 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 264c 636f 6d2f 6578
616d 706c 652f 6d79 6170 706c 6963 6174
696f 6e2f 4865 6c6c 6f57 6f72 6c64 3b01
0004 6d61 696e 0100 1628 5b4c 6a61 7661
2f6c 616e 672f 5374 7269 6e67 3b29 5601
0004 6172 6773 0100 135b 4c6a 6176 612f
6c61 6e67 2f53 7472 696e 673b 0100 0a53
6f75 7263 6546 696c 6501 000f 4865 6c6c
6f57 6f72 6c64 2e6a 6176 610c 0007 0008
0700 1c0c 001d 001e 0100 0c48 656c 6c6f
2057 6f72 6c64 2107 001f 0c00 2000 2101
0024 636f 6d2f 6578 616d 706c 652f 6d79
6170 706c 6963 6174 696f 6e2f 4865 6c6c
6f57 6f72 6c64 0100 106a 6176 612f 6c61
6e67 2f4f 626a 6563 7401 0010 6a61 7661
2f6c 616e 672f 5379 7374 656d 0100 036f
7574 0100 154c 6a61 7661 2f69 6f2f 5072
696e 7453 7472 6561 6d3b 0100 136a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
0100 0770 7269 6e74 6c6e 0100 1528 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
2956 0021 0005 0006 0000 0000 0002 0001
0007 0008 0001 0009 0000 002f 0001 0001
0000 0005 2ab7 0001 b100 0000 0200 0a00
0000 0600 0100 0000 0a00 0b00 0000 0c00
0100 0000 0500 0c00 0d00 0000 0900 0e00
0f00 0100 0900 0000 3700 0200 0100 0000
09b2 0002 1203 b600 04b1 0000 0002 000a
0000 000a 0002 0000 000c 0008 000d 000b
0000 000c 0001 0000 0009 0010 0011 0000
0001 0012 0000 0002 0013
复制代码
好家伙,这怎么可以看得懂?可是既然 java 虚拟机可以看懂,咱们也能够想办法看懂,用 javap -verbose HelloWorld.class 看起来就稍微简单一点:缓存
Last modified 2021-1-7; size 586 bytes
MD5 checksum bf91e508b76a0dc7d4c0250b0e55f75b
Compiled from "HelloWorld.java"
public class com.example.myapplication.HelloWorld
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/example/myapplication/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/example/myapplication/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/example/myapplication/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.example.myapplication.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/myapplication/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 12: 0
line 13: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
复制代码
.class 文件是一组以 8 位字节为基础单位的二进制流,各数据项目严格按照顺序紧凑地排列在 .class 文件中,中间没有添加任何分隔符,这使得整个 .class 文件中存储的内容几乎全都是程序须要的数据,没有空隙存在。至于具体有哪些内容,这里有一张表你们能够参考。性能优化
虚拟机加载 .class 文件,就是按照上面这样的规则去解析,最终解析的结果大体就是 javap -verbose 命令所生成的那样,若是你们只是阅读文章的话,建议你们本身要一点一点去尝试解析下,固然直播上我会带你们一块儿来看。markdown
在 JVM 虚拟机规范中并无规定加载的时机,可是却规定了初始化的时机,有如下五种状况须要必须当即对类进行初始化:网络
类的加载过程大体分为 5 个步骤:加载、验证、准备、解析和初始化,做为过来人早期我犯过很严重的错误,那就是为了面试习惯背,这样过段时间发现很容易忘记,并且开发中遇到相似的问题每每不知所措,所以但愿你们能好好的理解理解,这样才能作到一劳永逸:数据结构
双亲委派模型,咱们看一下 ClassLoader 的源码就能明白了,咱们公司的 Shadow 就是利用这个点来作插件类加载的,来公司后我自主学习看的第一个源码就是 Shadow ,顺便打个广告 Shadow 是一个腾讯自主研发的 Android 插件框架,通过线上亿级用户量检验。 Shadow 不只开源分享了插件技术的关键代码,还完整的分享了上线部署所须要的全部设计。与市面上其余插件框架相比,Shadow 主要具备如下特色:多线程
Kotlin 实现:core.loader,core.transform 核心代码彻底用 Kotlin 实现,代码简洁易维护。app
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 是否已经被加载了
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// 先从 parent 中加载
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
// 最后再从 this 加载
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
复制代码
了解了 .class 里面有啥,了解了 .class 怎么被解析加载,最后天然得了解下字节码命令是怎么执行的。在这以前咱们先得了解两个概念,什么是栈帧?什么是分派?
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操做数栈、动态链接和方法返回地址等信息。每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。每个栈帧都包括了局部变量表、操做数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中须要多大的局部变量表,多深的操做数栈都已经彻底肯定了,而且写入到方法表的 Code 属性之中,所以一个栈帧须要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。一个线程中的方法调用链可能会很长,不少方法都同时处于执行状态。对于执行引擎来讲,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method),执行引擎运行的全部字节码指令都只针对当前栈帧进行操做。
分派调用有多是静态的,也有多是动态的,咱们若是理解了这个,就会知道 Java 中的多态性是怎么实现的,像“重载”和“重写”等。Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符。前面两个就不作过多的解释了,至于方法描述符,它是由方法的参数类型以及返回类型所构成。在同一个类中,若是同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错。
能够看到,Java 虚拟机与 Java 语言不一样,它并不限制名字与参数类型相同,但返回类型不一样的方法出如今同一个类中,对于调用这些方法的字节码来讲,因为字节码所附带的方法描述符包含了返回类型,所以 Java 虚拟机可以准确地识别目标方法。
静态分派指的是在解析时便可以直接识别目标方法的状况,而动态分派则指的是须要在运行过程当中根据调用者的动态类型来识别目标方法的状况。Java 虚拟机中实际上是不存在重载概念的,由于在编译期间咱们就能肯定须要执行那个方法,若是非得区分那就是:重载被称为静态绑定或者编译时多态;而重写则被称为动态绑定。确切地说,Java 虚拟机中的静态分派指的是在解析时便可以直接识别目标方法的状况,而动态分派则指的是须要在运行过程当中根据调用者的动态类型来识别目标方法的状况。Java 虚拟机执行方法通常有五种指令:
有了这两个概念后,咱们就须要来看一个具体的实例了:
public class HelloWorld {
public static void main(String[] args){
int num1 = 100;
int num2 = 200;
int sum = sum(num1, num2);
System.out.println("sum = "+sum);
}
private static final int sum(int num1, int num2){
return num1 + num2;
}
}
复制代码
javap -verbose HelloWorld.class:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: iload_1
8: iload_2
9: invokestatic #2 // Method sum:(II)I
12: istore_3
13: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
16: new #4 // class java/lang/StringBuilder
19: dup
20: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
23: ldc #6 // String sum =
25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: iload_3
29: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 7
line 15: 13
line 16: 38
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 args [Ljava/lang/String;
3 36 1 num1 I
7 32 2 num2 I
13 26 3 sum I
复制代码
这个理解是比较重要的,虽然咱们在后面讲 asm 的时候会有傻瓜式操做,可是能不能理解怎么写为何要那么写,就靠咱们对着每一条指令集的理解了。咱们须要知道每一个指令表明的是什么意思,好比 bipush 100 表明把数字 100 压入栈中,istore_1 表明把刚压入栈的 100 放到局部变量表中。咱们须要清楚的知道每运行一个指令,当前栈和局部变量表中的数据是怎样变化的。
本文基本都是文字原理,你们要有耐心,若是可以理解实际上是很是简单的东西。这自己是三四次课的内容,我把其压缩到了一两次课来说。考虑到你们的水平不一,不少同窗可能会感受没有讲到位,所以你们能够去找些额外文章用来辅助理解,可是大的方向确定是这个方向。
视频地址:pan.baidu.com/s/1ozvNawIJ…
视频密码:q9kj