Java虛擬機(jī)運(yùn)行時(shí)棧的棧幀
Java虛擬機(jī)棧概述
Java虛擬機(jī)棧(Java Virtual Machine Stacks)是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:棧幀(Stack Frame)是用于支持Java虛擬機(jī)進(jìn)行方法調(diào)用和執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)棧中的棧元素。每個(gè)方法在執(zhí)行的同到都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
在編譯程序代碼的時(shí)候,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到方法表的Code屬性之中,因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。
每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬棧中從入棧到出棧的過程(說人話就是要執(zhí)行一個(gè)方法,將該方法的棧幀壓入棧頂,方法執(zhí)行完成其棧幀出棧)。在JVM里面,棧幀的操作只有兩種:出棧和入棧。正在被線程執(zhí)行的方法稱為當(dāng)前線程方法,而該方法的棧幀就稱為當(dāng)前幀,執(zhí)行引擎運(yùn)行時(shí)只對(duì)當(dāng)前棧幀有效。

下面對(duì)棧幀的每個(gè)組成部分分別介紹一下。
局部變量表
局部變量表(Local Variable Table)是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在Java程序編譯為Class文件時(shí)就在方法的code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法所需要分配的局部變量表的最大容量。
局部變量表的容量以變量槽(Variable Slot,下稱Slot)為最小單位,虛擬機(jī)規(guī)范中并沒有明確指明一個(gè)Slot應(yīng)占用的內(nèi)存空間大小,只是很有導(dǎo)向性地說到每個(gè)引都應(yīng)該能存放一個(gè)boolean、byte、char、short,int,float、reference或returnAddress類型的數(shù)據(jù),這8種數(shù)類都可以使用32位或更小的物理存來存放,但這種描述與明確指出 "每個(gè)Slot占用32位長(zhǎng)度的內(nèi)存空間" 是有一些差別的,它運(yùn)行Slot的長(zhǎng)度可以隨著處理器、操作系統(tǒng)或虛擬機(jī)的不同而發(fā)生變化。只要保證使在64位虛擬機(jī)中使用了64位的物理內(nèi)存空間去實(shí)現(xiàn)一個(gè)Slot,虛擬機(jī)仍要使用對(duì)齊和補(bǔ)白的手段讓Slot在外觀上看起來與32位擬機(jī)中的一致。
一個(gè)Slot可以存放一個(gè)32位以內(nèi)的數(shù)據(jù)類型,Java中占用32位以內(nèi)的數(shù)據(jù)類型有boolean、byte、char、short、float、reference和returnAddress8種類型。第7種reference類表示對(duì)一個(gè)對(duì)象實(shí)例的引用,虛擬機(jī)規(guī)范既沒有說明它的長(zhǎng)度,也沒有明確指出這種引用應(yīng)有怎樣的結(jié)構(gòu)。但是一般來說,虛擬機(jī)實(shí)現(xiàn)至少都應(yīng)當(dāng)能通過這個(gè)引用做到兩點(diǎn),一是從此引用直接或間接地查找到對(duì)象在Java堆中的數(shù)據(jù)存放的起始地址索引,二是此引用中直接或間接地查找到對(duì)象所屬數(shù)據(jù)類型在方法區(qū)中的存儲(chǔ)的類型信息。第8種即returnAddress類型目前已經(jīng)很少見了,現(xiàn)在已經(jīng)由異常表代替。
對(duì)于64位的數(shù)據(jù)類型,虛擬機(jī)會(huì)以高位對(duì)齊的方式為其分配兩個(gè)連續(xù)的引Slot空間。Java語言中明確的(reference類型則可能是32位也可能是64位),64位的數(shù)據(jù)類型只有l(wèi)ong和double兩種。虛擬機(jī)通過索引定位的方式使用局部變量表,索引值的范圍是從0開始至局部變量表最大的Slot數(shù)量。如果訪問的是32位數(shù)據(jù)類型的變量,索引 n 就代表了使用第n個(gè)Slot,如果是64位數(shù)據(jù)類型的變量,則說明會(huì)同時(shí)使用n和n+1兩個(gè)Slot對(duì)于兩個(gè)相鄰的共同存放一個(gè)64位數(shù)據(jù)的兩個(gè)Slot,不允許采用任何方式單獨(dú)訪問其中的某一個(gè),Java虛擬機(jī)規(guī)范中明確要求了如果遇到進(jìn)行這種操作的字節(jié)碼序列,虛擬機(jī)應(yīng)該在類加載的校驗(yàn)階段拋出異常。
如果是實(shí)例方法(非static的方法),那么局部變量表中第0位索引的Slot默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用"this"。其余參數(shù)則按照參數(shù)表的順序來排列,占用從1開始的局部變量Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot(比如方法method(int a1,inta2),參數(shù)表為a1和a2,則局部變量表索引0、1、2則分別存儲(chǔ)了this指針、a1、a2,如果方法內(nèi)部有其他內(nèi)部變量,則在局部變量表中存在a2之后的位置)。

為了盡可能節(jié)省棧幀空間,局部變量表中的Slot是可以重用的,方法體中定義的變量,其作用域并不一定會(huì)覆蓋整個(gè)方法體,如果當(dāng)前字節(jié)碼PC計(jì)數(shù)器的值已經(jīng)超出了某個(gè)變量的作用域,那這個(gè)變量對(duì)應(yīng)的Slot就可以交給其他變量使用。
局部變量不像的類成員變量那樣存在"準(zhǔn)備階段"。我們知道類變量有兩次賦初始值的過程,一次在準(zhǔn)備階段,賦予系統(tǒng)初始值;另外一次在初始化階段,賦予程序員定義的初始值。因此,即使在初始化階段程序員沒有為類變量賦值也沒有關(guān)系,類變量仍然具有一個(gè)確定的初始值。但局部變量就不一樣,如果一個(gè)局部變量定義了但沒有賦初始值是不能使用的,不要認(rèn)為Java中任何情況下都存在諸如整型變量默認(rèn)為0,布爾型變量默認(rèn)為false等這樣的默認(rèn)值。
操作數(shù)棧
操作數(shù)棧(Operand Stack)也常稱為操作棧,它是一個(gè)后入先出(Last In First out,LIFO)棧。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候?qū)懭氲絚ode屬性的max_stacks數(shù)據(jù)項(xiàng)中。操作數(shù)棧的每一個(gè)元素可以是任意的Java數(shù)據(jù)類型,包括long和double。32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2。在方法執(zhí)行的任何時(shí)候,操作數(shù)棧的深度都不會(huì)超過在maxstacks數(shù)據(jù)項(xiàng)中設(shè)定的最大值。
當(dāng)一個(gè)方法剛剛開始執(zhí)行的時(shí)候,這個(gè)方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會(huì)有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是出棧/入棧操作。例如,在做算術(shù)運(yùn)算的時(shí)候是通過操作數(shù)棧來進(jìn)行的,又或者在調(diào)用其他方法的時(shí)候是通過操作數(shù)棧來進(jìn)行參數(shù)傳遞的。舉個(gè)例子,整數(shù)加法的字節(jié)碼指令iadd在運(yùn)行的時(shí)候操作數(shù)棧中最接近棧頂?shù)膬蓚€(gè)元素已經(jīng)存入了兩個(gè)int型的數(shù)值,當(dāng)執(zhí)行這個(gè)指令時(shí),會(huì)將這兩個(gè)int值出棧并相加,然后將相加的結(jié)果入棧。
Java虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“?!本褪遣僮鲾?shù)棧。如果當(dāng)前線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。
動(dòng)態(tài)連接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接(Dynamic Linking)。Class文件的常量池中存有大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用作為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或者第一次使用的時(shí)候就轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另外一部分將在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,這部分稱為動(dòng)態(tài)連接。
Java代碼在進(jìn)行Javac編譯的時(shí)候,并不像C和C++那樣有“連接”這一步驟,而是在虛擬機(jī)加載Class文件的時(shí)候進(jìn)行動(dòng)態(tài)連接。也就是說,在Class文件中不會(huì)保存各個(gè)方法、字段的最終內(nèi)存布局信息,因此這些字段、方法的符號(hào)引用不經(jīng)過運(yùn)行期轉(zhuǎn)換的話無法得到真正的內(nèi)存入口地址,也就無法直接被虛擬機(jī)使用。當(dāng)虛擬機(jī)運(yùn)行時(shí),需要從常量池獲得對(duì)應(yīng)的符號(hào)引用,再在類創(chuàng)建時(shí)或運(yùn)行時(shí)解析、翻譯到具體的內(nèi)存地址之中。
Math math=new Math(); math.compute();//調(diào)用實(shí)例方法compute()
以上面兩行代碼為例,解釋一下動(dòng)態(tài)連接:math.compute()調(diào)用時(shí)compute()叫符號(hào),需要通過compute()這個(gè)符號(hào)去到常量池中去找到對(duì)應(yīng)方法的符號(hào)引用,運(yùn)行時(shí)將通過符號(hào)引用找到方法的字節(jié)碼指令的內(nèi)存地址。
方法的返回地址
當(dāng)一個(gè)方法開始執(zhí)行后,只有兩種方式可以退出這個(gè)方法。第一種方式是執(zhí)行引擎遇任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者),這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。另外一種退出方式是,在方法執(zhí)行過程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到處理,無論是Java虛擬機(jī)內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個(gè)方法使用異常完成出口的方式退出,是不會(huì)給它的上層調(diào)用者產(chǎn)生任何返回值的。
無論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時(shí)可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時(shí),調(diào)用者的PC計(jì)數(shù)器的值可以作為返回地址,棧幀中很可能會(huì)保存這個(gè)計(jì)數(shù)器值。而方法異常退出時(shí),返回地址是要通過異常處理器表來確定的,棧幀中一般不會(huì)保存這部分信息。方法退出的過程實(shí)際上就等同于把當(dāng)前棧幀出棧,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整pc計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。
結(jié)合javap命令理解棧幀
上面進(jìn)行了大段的文文字介紹,還是不太好理解,下面我們通過javap命令來分析一下方法中的操作指令、局部變量表、操作數(shù)棧等。
javap是jdk自帶的反解析工具。它的作用就是根據(jù)class字節(jié)碼文件,反解析出當(dāng)前類對(duì)應(yīng)的code區(qū)(匯編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。下面是其用法說明:
D:\wyonline\myworkspaces\framework\Test\bin\com\wkp\jvm>javap
用法: javap <options> <classes>
其中, 可能的選項(xiàng)包括:
-help --help -? 輸出此用法消息
-version 版本信息
-v -verbose 輸出附加信息
-l 輸出行號(hào)和本地變量表
-public 僅顯示公共類和成員
-protected 顯示受保護(hù)的/公共類和成員
-package 顯示程序包/受保護(hù)的/公共類
和成員 (默認(rèn))
-p -private 顯示所有類和成員
-c 對(duì)代碼進(jìn)行反匯編
-s 輸出內(nèi)部類型簽名
-sysinfo 顯示正在處理的類的
系統(tǒng)信息 (路徑, 大小, 日期, MD5 散列)
-constants 顯示最終常量
-classpath <path> 指定查找用戶類文件的位置
-cp <path> 指定查找用戶類文件的位置
-bootclasspath <path> 覆蓋引導(dǎo)類文件的位置
下面我們寫一個(gè)簡(jiǎn)單的Java程序:
package com.wkp.jvm;
public class Math {
public static final Integer CONSTANT=666;
public int compute() {//一個(gè)方法對(duì)應(yīng)一塊棧幀內(nèi)存區(qū)域
int a=3;
int b=5;
int c=(a+b)*10;
return c;
}
public static void main(String[] args) {
Math math=new Math();
math.compute();
}
}
然后進(jìn)入到Math.class所在目錄執(zhí)行: javap -c Math.class > Math.txt 命令,將Math.class字節(jié)碼文件反匯編然后輸出到Math.txt文件中:

然后我們查看Math.txt的內(nèi)容如下:我們重點(diǎn)分析下compute方法內(nèi)的指令,其內(nèi)部的指令后面我加了注釋(這里我是參考上一節(jié)的《JVM字節(jié)碼指令集大全及其介紹》,感興趣的話可以看一看),注釋中的棧就是指的棧幀中的操作數(shù)棧,本地變量表就是指的局部變量表。
Compiled from "Math.java"
public class com.wkp.jvm.Math {
public static final java.lang.Integer CONSTANT;
static {};
Code:
0: sipush 666
3: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
6: putstatic #16 // Field CONSTANT:Ljava/lang/Integer;
9: return
public com.wkp.jvm.Math();
Code:
0: aload_0
1: invokespecial #21 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_3 //將int類型的3推送至棧頂
1: istore_1 //將棧頂int類型數(shù)值(上面的3)出棧并存入第二個(gè)本地變量
2: iconst_5 //將int類型的5推送至棧頂
3: istore_2 //將棧頂int類型數(shù)值(上面的5)出棧并存入第三個(gè)本地變量
4: iload_1 //將第二個(gè)int型本地變量(上面的3)推送至棧頂
5: iload_2 //將第三個(gè)int型本地變量(上面的5)推送至棧頂
6: iadd //將棧頂兩int型數(shù)值出棧,然后相加并將結(jié)果壓入棧頂
7: bipush 10 //將常量值10推送至棧頂
9: imul //將棧頂兩int型數(shù)值出棧,然后相乘并將結(jié)果壓入棧頂
10: istore_3 //將棧頂int類型數(shù)值(上面的乘積)出棧并存入第四個(gè)本地變量
11: iload_3 //將第四個(gè)int類型本地變量推送至棧頂
12: ireturn //從當(dāng)前方法返回int類型值
public static void main(java.lang.String[]);
Code:
0: new #1 // class com/wkp/jvm/Math
3: dup
4: invokespecial #33 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #34 // Method compute:()I
12: pop
13: return
}
下面我們通過圖示簡(jiǎn)單表示一下上面compute方法中指令操作時(shí)關(guān)于本地變量表、操作數(shù)棧的情況:
我們先看下第一行 0: iconst_3 //將int類型的3推送至棧頂,可以看到下圖3已經(jīng)被入棧到操作數(shù)棧的棧頂。

我們?cè)倏聪碌诙?1: istore_1 //將棧頂int類型數(shù)值(上面的3)出棧并存入第二個(gè)本地變量,將上圖中棧頂?shù)?出棧然后存入本地表中第二個(gè)位置,如下圖所示:

第三行、第四行跟上面的一二行指令類似,第四行指令執(zhí)行后變成如下所示:

第五行、第六行中 4: iload_1 //將第二個(gè)int型本地變量(上面的3)推送至棧頂; 5: iload_2 //將第三個(gè)int型本地變量(上面的5)推送至棧頂,即將局部變量表中的3和5依次壓入棧頂,如下圖所示:

然后第七行執(zhí)行iadd操作,將棧頂?shù)膬蓚€(gè)int類型數(shù)據(jù)5和3出棧相加,將得到的和壓入棧頂,得到如下結(jié)果:

后面的指令操作過程與上面類似,執(zhí)行完第12行的iload_3指令之后,會(huì)得到如下圖所示:

關(guān)于局部變量表的信息,還可以通過javap -l 命令查看如下圖所示,另外還可以通過Idea中的jclasslib 查看。
LocalVariableTable表示的就是局部變量表的信息:
public int compute();
LineNumberTable:
line 8: 0
line 9: 2
line 10: 4
line 11: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/wkp/jvm/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I
我們還可以通過 javap -c Math.class > Math.txt 查看更多的信息如下:我們可以看到 9: invokevirtual #34 // Method compute:() 中的 #34 可以在常量池中找到 #34 = Methodref #1.#35 // com/wkp/jvm/Math.compute:()也就是方法的符號(hào)引用,運(yùn)行時(shí)通過符號(hào)引用解析出來方法的執(zhí)行指令的內(nèi)存地址,這個(gè)其實(shí)就是動(dòng)態(tài)連接。
Classfile /D:/wyonline/myworkspaces/framework/Test/bin/com/wkp/jvm/Math.class
Last modified 2019-8-24; size 761 bytes
MD5 checksum be0cdf4bcd037929d3fe0af86d44a837
Compiled from "Math.java"
public class com.wkp.jvm.Math
minor version: 0
major version: 52 //魔數(shù)
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: //常量池
#1 = Class #2 // com/wkp/jvm/Math
#2 = Utf8 com/wkp/jvm/Math
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 CONSTANT
#6 = Utf8 Ljava/lang/Integer;
#7 = Utf8 <clinit>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #11.#13 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#11 = Class #12 // java/lang/Integer
#12 = Utf8 java/lang/Integer
#13 = NameAndType #14:#15 // valueOf:(I)Ljava/lang/Integer;
#14 = Utf8 valueOf
#15 = Utf8 (I)Ljava/lang/Integer;
#16 = Fieldref #1.#17 // com/wkp/jvm/Math.CONSTANT:Ljava/lang/Integer;
#17 = NameAndType #5:#6 // CONSTANT:Ljava/lang/Integer;
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 <init>
#21 = Methodref #3.#22 // java/lang/Object."<init>":()V
#22 = NameAndType #20:#8 // "<init>":()V
#23 = Utf8 this
#24 = Utf8 Lcom/wkp/jvm/Math;
#25 = Utf8 compute
#26 = Utf8 ()I
#27 = Utf8 a
#28 = Utf8 I
#29 = Utf8 b
#30 = Utf8 c
#31 = Utf8 main
#32 = Utf8 ([Ljava/lang/String;)V
#33 = Methodref #1.#22 // com/wkp/jvm/Math."<init>":()V
#34 = Methodref #1.#35 // com/wkp/jvm/Math.compute:()I
#35 = NameAndType #25:#26 // compute:()I
#36 = Utf8 args
#37 = Utf8 [Ljava/lang/String;
#38 = Utf8 math
#39 = Utf8 SourceFile
#40 = Utf8 Math.java
{
public static final java.lang.Integer CONSTANT;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: sipush 666
3: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
6: putstatic #16 // Field CONSTANT:Ljava/lang/Integer;
9: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
public com.wkp.jvm.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #21 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wkp/jvm/Math;
public int compute();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_3
1: istore_1
2: iconst_5
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 8: 0
line 9: 2
line 10: 4
line 11: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/wkp/jvm/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #1 // class com/wkp/jvm/Math
3: dup
4: invokespecial #33 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #34 // Method compute:()I
12: pop
13: return
LineNumberTable:
line 15: 0
line 16: 8
line 17: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
8 6 1 math Lcom/wkp/jvm/Math;
}
SourceFile: "Math.java"
參考:《深入理解Java虛擬機(jī)第二版》、《Java虛擬機(jī)規(guī)范 JavaSE8版》
到此這篇關(guān)于Java虛擬機(jī)運(yùn)行時(shí)棧的棧幀的文章就介紹到這了,更多相關(guān)Java 虛擬機(jī) 棧幀內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringAOP切點(diǎn)函數(shù)實(shí)現(xiàn)原理詳解
這篇文章主要介紹了SpringAOP切點(diǎn)函數(shù)實(shí)現(xiàn)原理詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
SpringBoot?表單提交全局日期格式轉(zhuǎn)換器實(shí)現(xiàn)方式
這篇文章主要介紹了SpringBoot?表單提交全局日期格式轉(zhuǎn)換器,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04
淺析Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt)
這篇文章主要介紹了Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10
MyBatis學(xué)習(xí)教程(二)—如何使用MyBatis對(duì)users表執(zhí)行CRUD操作
這篇文章主要介紹了MyBatis學(xué)習(xí)教程(二)—如何使用MyBatis對(duì)users表執(zhí)行CRUD操作的相關(guān)資料,需要的朋友可以參考下2016-05-05
java開發(fā)中基于JDBC連接數(shù)據(jù)庫實(shí)例總結(jié)
這篇文章主要介紹了java開發(fā)中基于JDBC連接數(shù)據(jù)庫的方法,以實(shí)例形式較為詳細(xì)的總結(jié)分析了Java使用JDBC的具體步驟與注意事項(xiàng),并附帶了一個(gè)完整實(shí)例加以說明,需要的朋友可以參考下2015-11-11
springboot創(chuàng)建多module項(xiàng)目的實(shí)例
這篇文章主要介紹了springboot創(chuàng)建多module項(xiàng)目的實(shí)例代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
Java使用RedisTemplate操作Redis遇到的坑
這篇文章主要介紹了Java使用RedisTemplate操作Redis遇到的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
使用Java8中Optional機(jī)制的正確姿勢(shì)
我們知道 Java 8 增加了一些很有用的 API, 其中一個(gè)就是 Optional,下面這篇文章主要給大家介紹了關(guān)于如何正確使用Java8中Optional機(jī)制的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-11-11

