我要悄悄学习 Java 字节码指令,在成为技术大佬的路上一去不复返
大家好,我是二哥呀。
Java 字節(jié)碼指令是 JVM 體系中非常難啃的一塊硬骨頭,我估計(jì)有些讀者會有這樣的疑惑,“Java 字節(jié)碼難學(xué)嗎?我能不能學(xué)會啊?”
講良心話,不是我謙虛,一開始學(xué) Java 字節(jié)碼和 Java 虛擬機(jī)方面的知識我也感覺頭大!但硬著頭皮學(xué)了一陣子之后,突然就開竅了,覺得好有意思,尤其是明白了 Java 代碼在底層竟然是這樣執(zhí)行的時候,感覺既膨脹又飄飄然,渾身上下散發(fā)著自信的光芒!
我在 CSDN 共輸出了 100 多篇 Java 方面的文章,總字?jǐn)?shù)超過 30 萬字, 內(nèi)容風(fēng)趣幽默、通俗易懂,收獲了很多初學(xué)者的認(rèn)可和支持,內(nèi)容包括 Java 語法、Java 集合框架、Java 并發(fā)編程、Java 虛擬機(jī)等核心內(nèi)容。
為了幫助更多的 Java 初學(xué)者,我“一怒之下”就把這些文章重新整理并開源到了 GitHub,起名《教妹學(xué) Java》,聽起來是不是就很有趣?
GitHub 開源地址(歡迎 star):https://github.com/itwanger/jmx-java
Java 官方的虛擬機(jī) Hotspot 是基于棧的,而不是基于寄存器的。
基于棧的優(yōu)點(diǎn)是可移植性更好、指令更短、實(shí)現(xiàn)起來簡單,但不能隨機(jī)訪問棧中的元素,完成相同功能所需要的指令數(shù)也比寄存器的要多,需要頻繁的入棧和出棧。
基于寄存器的優(yōu)點(diǎn)是速度快,有利于程序運(yùn)行速度的優(yōu)化,但操作數(shù)需要顯式指定,指令也比較長。
Java 字節(jié)碼由操作碼和操作數(shù)組成。
- 操作碼(Opcode):一個字節(jié)長度(0-255,意味著指令集的操作碼總數(shù)不可能超過 256 條),代表著某種特定的操作含義。
- 操作數(shù)(Operands):零個或者多個,緊跟在操作碼之后,代表此操作需要的參數(shù)。
由于 Java 虛擬機(jī)是基于棧而不是寄存器的結(jié)構(gòu),所以大多數(shù)指令都只有一個操作碼。比如 aload_0(將局部變量表中下標(biāo)為 0 的數(shù)據(jù)壓入操作數(shù)棧中)就只有操作碼沒有操作數(shù),而 invokespecial #1(調(diào)用成員方法或者構(gòu)造方法,并傳遞常量池中下標(biāo)為 1 的常量)就是由操作碼和操作數(shù)組成的。
01、加載與存儲指令
加載(load)和存儲(store)相關(guān)的指令是使用最頻繁的指令,用于將數(shù)據(jù)從棧幀的局部變量表和操作數(shù)棧之間來回傳遞。
1)將局部變量表中的變量壓入操作數(shù)棧中
- xload_(x 為 i、l、f、d、a,n 默認(rèn)為 0 到 3),表示將第 n 個局部變量壓入操作數(shù)棧中。
- xload(x 為 i、l、f、d、a),通過指定參數(shù)的形式,將局部變量壓入操作數(shù)棧中,當(dāng)使用這個指令時,表示局部變量的數(shù)量可能超過了 4 個
解釋一下。
x 為操作碼助記符,表明是哪一種數(shù)據(jù)類型。見下表所示。
像 arraylength 指令,沒有操作碼助記符,它沒有代表數(shù)據(jù)類型的特殊字符,但操作數(shù)只能是一個數(shù)組類型的對象。
大部分的指令都不支持 byte、short 和 char,甚至沒有任何指令支持 boolean 類型。編譯器會將 byte 和 short 類型的數(shù)據(jù)帶符號擴(kuò)展(Sign-Extend)為 int 類型,將 boolean 和 char 零位擴(kuò)展(Zero-Extend)為 int 類型。
舉例來說。
private void load(int age, String name, long birthday, boolean sex) {System.out.println(age + name + birthday + sex); }通過 jclasslib 看一下 load() 方法(4 個參數(shù))的字節(jié)碼指令。
- iload_1:將局部變量表中下標(biāo)為 1 的 int 變量壓入操作數(shù)棧中。
- aload_2:將局部變量表中下標(biāo)為 2 的引用數(shù)據(jù)類型變量(此時為 String)壓入操作數(shù)棧中。
- lload_3:將局部變量表中下標(biāo)為 3 的 long 型變量壓入操作數(shù)棧中。
- iload 5:將局部變量表中下標(biāo)為 5 的 int 變量(實(shí)際為 boolean)壓入操作數(shù)棧中。
通過查看局部變量表就能關(guān)聯(lián)上了。
2)將常量池中的常量壓入操作數(shù)棧中
根據(jù)數(shù)據(jù)類型和入棧內(nèi)容的不同,此類又可以細(xì)分為 const 系列、push 系列和 Idc 指令。
const 系列,用于特殊的常量入棧,要入棧的常量隱含在指令本身。
push 系列,主要包括 bipush 和 sipush,前者接收 8 位整數(shù)作為參數(shù),后者接收 16 位整數(shù)。
Idc 指令,當(dāng) const 和 push 不能滿足的時候,萬能的 Idc 指令就上場了,它接收一個 8 位的參數(shù),指向常量池中的索引。
- Idc_w:接收兩個 8 位數(shù),索引范圍更大。
- 如果參數(shù)是 long 或者 double,使用 Idc2_w 指令。
舉例來說。
public void pushConstLdc() {// 范圍 [-1,5]int iconst = -1;// 范圍 [-128,127]int bipush = 127;// 范圍 [-32768,32767]int sipush= 32767;// 其他 intint ldc = 32768;String aconst = null;String IdcString = "沉默王二"; }通過 jclasslib 看一下 pushConstLdc() 方法的字節(jié)碼指令。
- iconst_m1:將 -1 入棧。范圍 [-1,5]。
- bipush 127:將 127 入棧。范圍 [-128,127]。
- sipush 32767:將 32767 入棧。范圍 [-32768,32767]。
- ldc #6 <32768>:將常量池中下標(biāo)為 6 的常量 32768 入棧。
- aconst_null:將 null 入棧。
- ldc #7 <沉默王二>:將常量池中下標(biāo)為 7 的常量“沉默王二”入棧。
3)將棧頂?shù)臄?shù)據(jù)出棧并裝入局部變量表中
主要是用來給局部變量賦值,這類指令主要以 store 的形式存在。
- xstore_(x 為 i、l、f、d、a,n 默認(rèn)為 0 到 3)
- xstore(x 為 i、l、f、d、a)
明白了 xload_ 和 xload,再看 xstore_ 和 xstore 就會輕松得多,作用反了一下而已。
大家來想一個問題,為什么要有 xstore_ 和 xload_ 呢?它們的作用和 xstore n、xload n 不是一樣的嗎?
xstore_ 和 xstore n 的區(qū)別在于,前者相當(dāng)于只有操作碼,占用 1 個字節(jié);后者相當(dāng)于由操作碼和操作數(shù)組成,操作碼占 1 個字節(jié),操作數(shù)占 2 個字節(jié),一共占 3 個字節(jié)。
由于局部變量表中前幾個位置總是非常常用,雖然 xstore_<n> 和 xload_<n> 增加了指令數(shù)量,但字節(jié)碼的體積變小了!
舉例來說。
public void store(int age, String name) {int temp = age + 2;String str = name; }通過 jclasslib 看一下 store() 方法的字節(jié)碼指令。
- istore_3:從操作數(shù)中彈出一個整數(shù),并把它賦值給局部變量表中索引為 3 的變量。
- astore 4:從操作數(shù)中彈出一個引用數(shù)據(jù)類型,并把它賦值給局部變量表中索引為 4 的變量。
通過查看局部變量表就能關(guān)聯(lián)上了。
02、算術(shù)指令
算術(shù)指令用于對兩個操作數(shù)棧上的值進(jìn)行某種特定運(yùn)算,并把結(jié)果重新壓入操作數(shù)棧。可以分為兩類:整型數(shù)據(jù)的運(yùn)算指令和浮點(diǎn)數(shù)據(jù)的運(yùn)算指令。
需要注意的是,數(shù)據(jù)運(yùn)算可能會導(dǎo)致溢出,比如兩個很大的正整數(shù)相加,很可能會得到一個負(fù)數(shù)。但 Java 虛擬機(jī)規(guī)范中并沒有對這種情況給出具體結(jié)果,因此程序是不會顯式報(bào)錯的。所以,大家在開發(fā)過程中,如果涉及到較大的數(shù)據(jù)進(jìn)行加法、乘法運(yùn)算的時候,一定要注意!
當(dāng)發(fā)生溢出時,將會使用有符號的無窮大 Infinity 來表示;如果某個操作結(jié)果沒有明確的數(shù)學(xué)定義的話,將會使用 NaN 值來表示。而且所有使用 NaN 作為操作數(shù)的算術(shù)操作,結(jié)果都會返回 NaN。
舉例來說。
public void infinityNaN() {int i = 10;double j = i / 0.0;System.out.println(j); // Infinitydouble d1 = 0.0;double d2 = d1 / 0.0;System.out.println(d2); // NaN }- 任何一個非零的數(shù)除以浮點(diǎn)數(shù) 0(注意不是 int 類型),可以想象結(jié)果是無窮大 Infinity 的。
- 把這個非零的數(shù)換成 0 的時候,結(jié)果又不太好定義,就用 NaN 值來表示。
Java 虛擬機(jī)提供了兩種運(yùn)算模式:
- 向最接近數(shù)舍入:在進(jìn)行浮點(diǎn)數(shù)運(yùn)算時,所有的結(jié)果都必須舍入到一個適當(dāng)?shù)木?#xff0c;不是特別精確的結(jié)果必須舍入為可被表示的最接近的精確值,如果有兩種可表示的形式與該值接近,將優(yōu)先選擇最低有效位為零的(類似四舍五入)。
- 向零舍入:將浮點(diǎn)數(shù)轉(zhuǎn)換為整數(shù)時,采用該模式,該模式將在目標(biāo)數(shù)值類型中選擇一個最接近但是不大于原值的數(shù)字作為最精確的舍入結(jié)果(類似取整)。
我把所有的算術(shù)指令列一下:
- 加法指令:iadd、ladd、fadd、dadd
- 減法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem
- 自增指令:iinc
舉例來說。
public void calculate(int age) {int add = age + 1;int sub = age - 1;int mul = age * 2;int div = age / 3;int rem = age % 4;age++;age--; }通過 jclasslib 看一下 calculate() 方法的字節(jié)碼指令。
- iadd,加法
- isub,減法
- imul,乘法
- idiv,除法
- irem,取余
- iinc,自增的時候 +1,自減的時候 -1
03、類型轉(zhuǎn)換指令
可以分為兩種:
1)寬化,小類型向大類型轉(zhuǎn)換,比如 int–>long–>float–>double,對應(yīng)的指令有:i2l、i2f、i2d、l2f、l2d、f2d。
- 從 int 到 long,或者從 int 到 double,是不會有精度丟失的;
- 從 int、long 到 float,或者 long 到 double 時,可能會發(fā)生精度丟失;
- 從 byte、char 和 short 到 int 的寬化類型轉(zhuǎn)換實(shí)際上是隱式發(fā)生的,這樣可以減少字節(jié)碼指令,畢竟字節(jié)碼指令只有 256 個,占一個字節(jié)。
2)窄化,大類型向小類型轉(zhuǎn)換,比如從 int 類型到 byte、short 或者 char,對應(yīng)的指令有:i2b、i2s、i2c;從 long 到 int,對應(yīng)的指令有:l2i;從 float 到 int 或者 long,對應(yīng)的指令有:f2i、f2l;從 double 到 int、long 或者 float,對應(yīng)的指令有:d2i、d2l、d2f。
- 窄化很可能會發(fā)生精度丟失,畢竟是不同的數(shù)量級;
- 但 Java 虛擬機(jī)并不會因此拋出運(yùn)行時異常。
舉例來說。
public void updown() {int i = 10;double d = i;float f = 10f;long ong = (long)f; }通過 jclasslib 看一下 updown() 方法的字節(jié)碼指令。
- i2d,int 寬化為 double
- f2l, float 窄化為 long
04、對象的創(chuàng)建和訪問指令
Java 是一門面向?qū)ο蟮木幊陶Z言,那么 Java 虛擬機(jī)是如何從字節(jié)碼層面進(jìn)行支持的呢?
1)創(chuàng)建指令
數(shù)組也是一種對象,但它創(chuàng)建的字節(jié)碼指令和普通的對象不同。創(chuàng)建數(shù)組的指令有三種:
- newarray:創(chuàng)建基本數(shù)據(jù)類型的數(shù)組
- anewarray:創(chuàng)建引用類型的數(shù)組
- multianewarray:創(chuàng)建多維數(shù)組
普通對象的創(chuàng)建指令只有一個,就是 new,它會接收一個操作數(shù),指向常量池中的一個索引,表示要創(chuàng)建的類型。
舉例來說。
public void newObject() {String name = new String("沉默王二");File file = new File("無愁河的浪蕩漢子.book");int [] ages = {}; }通過 jclasslib 看一下 newObject() 方法的字節(jié)碼指令。
- new #13 <java/lang/String>,創(chuàng)建一個 String 對象。
- new #15 <java/io/File>,創(chuàng)建一個 File 對象。
- newarray 10 (int),創(chuàng)建一個 int 類型的數(shù)組。
2)字段訪問指令
字段可以分為兩類,一類是成員變量,一類是靜態(tài)變量(static 關(guān)鍵字修飾的),所以字段訪問指令可以分為兩類:
- 訪問靜態(tài)變量:getstatic、putstatic。
- 訪問成員變量:getfield、putfield,需要創(chuàng)建對象后才能訪問。
舉例來說。
public class Writer {private String name;static String mark = "作者";public static void main(String[] args) {print(mark);Writer w = new Writer();print(w.name);}public static void print(String arg) {System.out.println(arg);} }通過 jclasslib 看一下 main() 方法的字節(jié)碼指令。
- getstatic #2 <com/itwanger/jvm/Writer.mark>,訪問靜態(tài)變量 mark
- getfield #6 <com/itwanger/jvm/Writer.name>,訪問成員變量 name
05、方法調(diào)用和返回指令
方法調(diào)用指令有 5 個,分別用于不同的場景:
- invokevirtual:用于調(diào)用對象的成員方法,根據(jù)對象的實(shí)際類型進(jìn)行分派,支持多態(tài)。
- invokeinterface:用于調(diào)用接口方法,會在運(yùn)行時搜索由特定對象實(shí)現(xiàn)的接口方法進(jìn)行調(diào)用。
- invokespecial:用于調(diào)用一些需要特殊處理的方法,包括構(gòu)造方法、私有方法和父類方法。
- invokestatic:用于調(diào)用靜態(tài)方法。
- invokedynamic:用于在運(yùn)行時動態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法,并執(zhí)行。
舉例來說。
public class InvokeExamples {private void run() {List ls = new ArrayList();ls.add("難頂");ArrayList als = new ArrayList();als.add("學(xué)不動了");}public static void print() {System.out.println("invokestatic");}public static void main(String[] args) {print();InvokeExamples invoke = new InvokeExamples();invoke.run();} }我們用 javap -c InvokeExamples.class 來反編譯一下。
Compiled from "InvokeExamples.java" public class com.itwanger.jvm.InvokeExamples {public com.itwanger.jvm.InvokeExamples();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnprivate void run();Code:0: new #2 // class java/util/ArrayList3: dup4: invokespecial #3 // Method java/util/ArrayList."<init>":()V7: astore_18: aload_19: ldc #4 // String 難頂11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z16: pop17: new #2 // class java/util/ArrayList20: dup21: invokespecial #3 // Method java/util/ArrayList."<init>":()V24: astore_225: aload_226: ldc #6 // String 學(xué)不動了28: invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z31: pop32: returnpublic static void print();Code:0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #9 // String invokestatic5: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnpublic static void main(java.lang.String[]);Code:0: invokestatic #11 // Method print:()V3: new #12 // class com/itwanger/jvm/InvokeExamples6: dup7: invokespecial #13 // Method "<init>":()V10: astore_111: aload_112: invokevirtual #14 // Method run:()V15: return }InvokeExamples 類有 4 個方法,包括缺省的構(gòu)造方法在內(nèi)。
1)InvokeExamples() 構(gòu)造方法中
缺省的構(gòu)造方法內(nèi)部會調(diào)用超類 Object 的初始化構(gòu)造方法:
`invokespecial #1 // Method java/lang/Object."<init>":()V`2)成員方法 run() 中
invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z由于 ls 變量的引用類型為接口 List,所以 ls.add() 調(diào)用的是 invokeinterface 指令,等運(yùn)行時再確定是不是接口 List 的實(shí)現(xiàn)對象 ArrayList 的 add() 方法。
invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z由于 als 變量的引用類型已經(jīng)確定為 ArrayList,所以 als.add() 方法調(diào)用的是 invokevirtual 指令。
3)main() 方法中
invokestatic #11 // Method print:()Vprint() 方法是靜態(tài)的,所以調(diào)用的是 invokestatic 指令。
方法返回指令根據(jù)方法的返回值類型進(jìn)行區(qū)分,常見的返回指令見下圖。
06、操作數(shù)棧管理指令
常見的操作數(shù)棧管理指令有 pop、dup 和 swap。
- 將一個或兩個元素從棧頂彈出,并且直接廢棄,比如 pop,pop2;
- 復(fù)制棧頂?shù)囊粋€或兩個數(shù)值并將其重新壓入棧頂,比如 dup,dup2,dup_×1,dup2_×1,dup_×2,dup2_×2;
- 將棧最頂端的兩個槽中的數(shù)值交換位置,比如 swap。
這些指令不需要指明數(shù)據(jù)類型,因?yàn)槭前凑瘴恢脡喝牒蛷棾龅摹?/p>
舉例來說。
public class Dup {int age;public int incAndGet() {return ++age;} }通過 jclasslib 看一下 incAndGet() 方法的字節(jié)碼指令。
- aload_0:將 this 入棧。
- dup:復(fù)制棧頂?shù)?this。
- getfield #2:將常量池中下標(biāo)為 2 的常量加載到棧上,同時將一個 this 出棧。
- iconst_1:將常量 1 入棧。
- iadd:將棧頂?shù)膬蓚€值相加后出棧,并將結(jié)果放回棧上。
- dup_x1:復(fù)制棧頂?shù)脑?#xff0c;并將其插入 this 下面。
- putfield #2: 將棧頂?shù)膬蓚€元素出棧,并將其賦值給字段 age。
- ireturn:將棧頂?shù)脑爻鰲7祷亍?/li>
07、控制轉(zhuǎn)移指令
控制轉(zhuǎn)移指令包括:
- 比較指令,比較棧頂?shù)膬蓚€元素的大小,并將比較結(jié)果入棧。
- 條件跳轉(zhuǎn)指令,通常和比較指令一塊使用,在條件跳轉(zhuǎn)指令執(zhí)行前,一般先用比較指令進(jìn)行棧頂元素的比較,然后進(jìn)行條件跳轉(zhuǎn)。
- 比較條件轉(zhuǎn)指令,類似于比較指令和條件跳轉(zhuǎn)指令的結(jié)合體,它將比較和跳轉(zhuǎn)兩個步驟合二為一。
- 多條件分支跳轉(zhuǎn)指令,專為 switch-case 語句設(shè)計(jì)的。
- 無條件跳轉(zhuǎn)指令,目前主要是 goto 指令。
1)比較指令
比較指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp,指令的第一個字母代表的含義分別是 double、float、long。注意,沒有 int 類型。
對于 double 和 float 來說,由于 NaN 的存在,有兩個版本的比較指令。拿 float 來說,有 fcmpg 和 fcmpl,區(qū)別在于,如果遇到 NaN,fcmpg 會將 1 壓入棧,fcmpl 會將 -1 壓入棧。
舉例來說。
public void lcmp(long a, long b) {if(a > b){} }通過 jclasslib 看一下 lcmp() 方法的字節(jié)碼指令。
lcmp 用于兩個 long 型的數(shù)據(jù)進(jìn)行比較。
2)條件跳轉(zhuǎn)指令
這些指令都會接收兩個字節(jié)的操作數(shù),它們的統(tǒng)一含義是,彈出棧頂元素,測試它是否滿足某一條件,滿足的話,跳轉(zhuǎn)到對應(yīng)位置。
對于 long、float 和 double 類型的條件分支比較,會先執(zhí)行比較指令返回一個整形值到操作數(shù)棧中后再執(zhí)行 int 類型的條件跳轉(zhuǎn)指令。
對于 boolean、byte、char、short,以及 int,則直接使用條件跳轉(zhuǎn)指令來完成。
舉例來說。
public void fi() {int a = 0;if (a == 0) {a = 10;} else {a = 20;} }通過 jclasslib 看一下 fi() 方法的字節(jié)碼指令。
3 ifne 12 (+9) 的意思是,如果棧頂?shù)脑夭坏扔?0,跳轉(zhuǎn)到第 12(3+9)行 12 bipush 20。
3)比較條件轉(zhuǎn)指令
前綴“if_”后,以字符“i”開頭的指令針對 int 型整數(shù)進(jìn)行操作,以字符“a”開頭的指令表示對象的比較。
舉例來說。
public void compare() {int i = 10;int j = 20;System.out.println(i > j); }通過 jclasslib 看一下 compare() 方法的字節(jié)碼指令。
11 if_icmple 18 (+7) 的意思是,如果棧頂?shù)膬蓚€ int 類型的數(shù)值比較的話,如果前者小于后者時跳轉(zhuǎn)到第 18 行(11+7)。
4)多條件分支跳轉(zhuǎn)指令
主要有 tableswitch 和 lookupswitch,前者要求多個條件分支值是連續(xù)的,它內(nèi)部只存放起始值和終止值,以及若干個跳轉(zhuǎn)偏移量,通過給定的操作數(shù) index,可以立即定位到跳轉(zhuǎn)偏移量位置,因此效率比較高;后者內(nèi)部存放著各個離散的 case-offset 對,每次執(zhí)行都要搜索全部的 case-offset 對,找到匹配的 case 值,并根據(jù)對應(yīng)的 offset 計(jì)算跳轉(zhuǎn)地址,因此效率較低。
舉例來說。
public void switchTest(int select) {int num;switch (select) {case 1:num = 10;break;case 2:case 3:num = 30;break;default:num = 40;} }通過 jclasslib 看一下 switchTest() 方法的字節(jié)碼指令。
case 2 的時候沒有 break,所以 case 2 和 case 3 是連續(xù)的,用的是 tableswitch。如果等于 1,跳轉(zhuǎn)到 28 行;如果等于 2 和 3,跳轉(zhuǎn)到 34 行,如果是 default,跳轉(zhuǎn)到 40 行。
5)無條件跳轉(zhuǎn)指令
goto 指令接收兩個字節(jié)的操作數(shù),共同組成一個帶符號的整數(shù),用于指定指令的偏移量,指令執(zhí)行的目的就是跳轉(zhuǎn)到偏移量給定的位置處。
前面的例子里都出現(xiàn)了 goto 的身影,也很好理解。如果指令的偏移量特別大,超出了兩個字節(jié)的范圍,可以使用指令 goto_w,接收 4 個字節(jié)的操作數(shù)。
巨人的肩膀:
https://segmentfault.com/a/1190000037628881
除了以上這些指令,還有異常處理指令和同步控制指令,我打算吊一吊大家的胃口,大家可以期待一波~~
(騷操作)
路漫漫其修遠(yuǎn)兮,吾將上下而求索
想要走得更遠(yuǎn),Java 字節(jié)碼這塊就必須得硬碰硬地吃透,希望二哥的這些分享可以幫助到大家~
叨逼叨
二哥在 CSDN 上寫了很多 Java 方面的系列文章,有 Java 核心語法、Java 集合框架、Java IO、Java 并發(fā)編程、Java 虛擬機(jī)等,也算是體系完整了。
為了能幫助到更多的 Java 初學(xué)者,二哥把自己連載的《教妹學(xué)Java》開源到了 GitHub,盡管只整理了 50 篇,發(fā)現(xiàn)字?jǐn)?shù)已經(jīng)來到了 10 萬+,內(nèi)容更是沒得說,通俗易懂、風(fēng)趣幽默、圖文并茂。
GitHub 開源地址(歡迎 star):https://github.com/itwanger/jmx-java
如果有幫助的話,還請給二哥點(diǎn)個贊,這將是我繼續(xù)分享下去的最強(qiáng)動力!
總結(jié)
以上是生活随笔為你收集整理的我要悄悄学习 Java 字节码指令,在成为技术大佬的路上一去不复返的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 常用Python模块下载网站
- 下一篇: PMP考试通过率