開篇就提到效能優化涉及的範圍會很廣,考慮後面須要常常用到 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