深入解析JVM之內(nèi)存結(jié)構(gòu)及字符串常量池(推薦)
前言
Java作為一種平臺(tái)無(wú)關(guān)性的語(yǔ)言,其主要依靠于Java虛擬機(jī)——JVM,我們寫好的代碼會(huì)被編譯成class文件,再由JVM進(jìn)行加載、解析、執(zhí)行,而JVM有統(tǒng)一的規(guī)范,所以我們不需要像C++那樣需要程序員自己關(guān)注平臺(tái),大大方便了我們的開發(fā)。另外,能夠運(yùn)行在JVM上的并只有Java,只要能夠編譯生成合乎規(guī)范的class文件的語(yǔ)言都是可以跑在JVM上的。而作為一名Java開發(fā),JVM是我們必須要學(xué)習(xí)了解的基礎(chǔ),也是通向高級(jí)及更高層次的必修課;但JVM的體系非常龐大,且術(shù)語(yǔ)非常多,所以初學(xué)者對(duì)此非常的頭疼。本系列文章就是筆者自己對(duì)于JVM的核心知識(shí)(內(nèi)存結(jié)構(gòu)、類加載、對(duì)象創(chuàng)建、垃圾回收等)以及性能調(diào)優(yōu)的學(xué)習(xí)總結(jié),另外未特別指出本系列文章都是基于HotSpot虛擬機(jī)進(jìn)行講解。
正文
JVM包含了非常多的知識(shí),比較核心的有內(nèi)存結(jié)構(gòu)、類加載、類文件結(jié)構(gòu)、垃圾回收、執(zhí)行 引擎、性能調(diào)優(yōu)、監(jiān)控等等這些知識(shí),但所有的功能都是圍繞著內(nèi)存結(jié)構(gòu)展開的,因?yàn)槲覀兙幾g后的代碼信息在運(yùn)行過(guò)程中都是存在于JVM自身的內(nèi)存區(qū)域中的,并且這塊區(qū)域相當(dāng)?shù)闹悄?,不需要C++那樣需要我們自己手動(dòng)釋放內(nèi)存,它實(shí)現(xiàn)了自動(dòng)垃圾回收機(jī)制,這也是Java廣受喜愛的原因之一。因此,學(xué)習(xí)JVM我們首先就得了解其內(nèi)存結(jié)構(gòu),熟悉包含的東西,才能更好的學(xué)習(xí)后面的知識(shí)。
內(nèi)存結(jié)構(gòu)
如上圖所示,JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)(即內(nèi)存結(jié)構(gòu))整體上劃分為線程私有和線程共享區(qū)域,線程私有的區(qū)域生命周期與線程相同,線程共享區(qū)域則存在于整個(gè)運(yùn)行期間 。而按照J(rèn)VM規(guī)范細(xì)分則分為程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧、方法區(qū)和堆五大區(qū)域(直接內(nèi)存不屬于JVM)。注意這只是規(guī)范定義需要存在的區(qū)域,具體的實(shí)現(xiàn)則不在規(guī)范的定義中。
1. 程序計(jì)數(shù)器
如其名,這個(gè)部件就是用來(lái)記錄程序執(zhí)行的地址的,循環(huán)、跳轉(zhuǎn)、異常等等需要依靠它。為什么它是線程私有的呢?以單核CPU為例,多線程在執(zhí)行時(shí)是輪流執(zhí)行的,那么當(dāng)線程暫停后恢復(fù)就需要程序計(jì)數(shù)器恢復(fù)到暫停前的執(zhí)行位置繼續(xù)執(zhí)行,所以必然是每個(gè)線程對(duì)應(yīng)一個(gè)。由于它只需記錄一個(gè)執(zhí)行地址,所以它是五大區(qū)域中唯一一個(gè)不會(huì)出現(xiàn)OOM(內(nèi)存溢出)的區(qū)域。另外它是控制我們JAVA代碼的執(zhí)行的,在調(diào)用native方法時(shí)該計(jì)數(shù)器就沒有作用了,而是會(huì)由操作系統(tǒng)的計(jì)數(shù)器控制。
2. 虛擬機(jī)棧
虛擬機(jī)棧是方法執(zhí)行的內(nèi)存區(qū)域,每調(diào)用一個(gè)方法都會(huì)生成一個(gè)棧幀壓入棧中,當(dāng)方法執(zhí)行完成才會(huì)彈出棧。棧幀中又包含了局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口。其中局部變量表就是用來(lái)存儲(chǔ)局部變量的(基本類型值和對(duì)象的引用),每一個(gè)位置32位,而像long/double這樣的變量則需要占用兩個(gè)槽位;操作數(shù)棧則類似于緩存,用于存儲(chǔ)執(zhí)行引擎在計(jì)算時(shí)需要用到的局部變量;動(dòng)態(tài)鏈接這里暫時(shí)不講,后面的章節(jié)會(huì)詳細(xì)分析;方法出口則包含異常出口和正常出口以及返回地址。下面來(lái)看三個(gè)方法示例分別展示棧和棧幀的運(yùn)行原理。
入棧出棧過(guò)程
public class ClassDemo1 { public static void main(String[] args) { new ClassDemo1().a(); } static void a() { new ClassDemo1().b(); } static void b() { new ClassDemo1().c(); } static void c() {} }
如上所示的方法調(diào)用入棧出棧的過(guò)程如下:
棧幀執(zhí)行原理
public class ClassDemo2 { public int work() { int x = 3; int y = 5; int z = (x + y) * 10; return z; } public static void main(String[] args) { new ClassDemo2().work(); } }
上面只是一簡(jiǎn)單的計(jì)算程序,通過(guò)javap -c ClassDemo2.class命令反編譯后看看生成的字節(jié)碼:
public class cn.dark.ClassDemo { public cn.dark.ClassDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int work(); Code: 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 public static void main(java.lang.String[]); Code: 0: new #2 // class cn/dark/ClassDemo 3: dup 4: invokespecial #3 // Method "<init>":()V 7: invokevirtual #4 // Method work:()I 10: pop 11: return }
主要看到work方法中,挨個(gè)來(lái)解釋(字節(jié)碼指令釋義可以參照這篇文章):執(zhí)行引擎首先通過(guò)iconst_3將常量3存入到操作數(shù)棧中,然后通過(guò)istore_1將該值從操作數(shù)棧中取出并存入到局部變量表的1號(hào)位(注意局部變量表示從0號(hào)開始的,但0號(hào)位默認(rèn)存儲(chǔ)了this變量);接著常量5執(zhí)行同樣的操作,完成后局部變量表中就存了3個(gè)變量(this、3、5);之后通過(guò)iload指令將局表變量表對(duì)應(yīng)位置的變量加載到操作數(shù)棧中,因?yàn)檫@里有括號(hào),所以先加載兩個(gè)變量到操作數(shù)棧并執(zhí)行括號(hào)中的加法,即調(diào)用iadd加法指令(所有二元算數(shù)指令會(huì)從操作數(shù)棧中取出頂部的兩個(gè)變量進(jìn)行計(jì)算,計(jì)算結(jié)果自動(dòng)加入到棧中);接著又將常量10壓入到棧中,繼續(xù)調(diào)用imul乘法指令,完成后需要通過(guò)istore命令再將結(jié)果存入到局部變量表中,最后通過(guò)ireturn返回(不管我們方法是否定義了返回值都會(huì)調(diào)用該指令,只是當(dāng)我們定義了返回值時(shí),首先會(huì)通過(guò)iload指令加載局部變量表的值并返回給調(diào)用者)。以上就是棧幀的運(yùn)行原理。
該區(qū)域同樣是線程私有,每個(gè)線程對(duì)應(yīng)會(huì)生成一個(gè)棧,并且每個(gè)棧默認(rèn)大小是1M,但也不是絕對(duì),根據(jù)操作系統(tǒng)不同會(huì)有所不
一樣,另外可以用-Xss控制大小,官方文檔對(duì)該該參數(shù)解釋如下:
既然可以控制大小,那么這塊區(qū)域自然就會(huì)存在內(nèi)存不足的情況,對(duì)于棧當(dāng)內(nèi)存不足時(shí)會(huì)出現(xiàn)下面兩種異常:
- 棧溢出(StackOverflowError)
- 內(nèi)存溢出(OutOfMemoryError)
為什么會(huì)有兩種異常呢?在周志明的《深入理解Java虛擬機(jī)》一書中講到,在單線程環(huán)境下只會(huì)出現(xiàn)StackOverflowError異常,即棧幀填滿了?;蚓植孔兞勘磉^(guò)大;而OutOfMemoryError只有當(dāng)多線程情況下,無(wú)節(jié)制的創(chuàng)建多個(gè)棧才會(huì)出現(xiàn),因?yàn)椴僮飨到y(tǒng)對(duì)于每個(gè)進(jìn)程是有內(nèi)存限制的,即超出了進(jìn)程可用的內(nèi)存,無(wú)法創(chuàng)建新的棧。
- 棧幀共享機(jī)制
通過(guò)上文我們知道同一個(gè)線程內(nèi)每個(gè)方法的調(diào)用會(huì)對(duì)應(yīng)生成相應(yīng)的棧幀,而棧幀又包含了局部變量表和操作數(shù)棧等內(nèi)容,那么當(dāng)方法間傳遞參數(shù)時(shí)是否可以優(yōu)化,使得它們共享一部分內(nèi)存空間呢?答案是肯定的,像下面這段代碼:
public int work(int x) throws Exception{ int z =(x+5)*10;// 參數(shù)會(huì)按照順序放到局部變量表 Thread.sleep(Integer.MAX_VALUE); return z; } public static void main(String[] args)throws Exception { JVMStack jvmStack = new JVMStack(); jvmStack.work(10);//10 放入操作數(shù)棧 }
在main方法中首先會(huì)把10放入操作數(shù)棧然后傳遞給work方法,作為參數(shù),會(huì)按照順序放入到局部變量表中,所以x會(huì)放到局部變量表的1號(hào)位(0號(hào)位是this),而此時(shí)通過(guò)HSDB工具查看這時(shí)的棧調(diào)用信息會(huì)發(fā)現(xiàn)如下情況:
如上圖所示,中間一小塊用紅框圈起來(lái)的就是兩個(gè)棧幀共享的內(nèi)存區(qū)域,即work的局部變量表和main的操作數(shù)棧的一部分。
3. 本地方法棧
和虛擬機(jī)棧是一樣的,只不過(guò)該區(qū)域是用來(lái)執(zhí)行本地本地方法的,有些虛擬機(jī)甚至直接將其和虛擬機(jī)棧合二為一,如HotSpot。(通過(guò)上面的圖也可以看到,最上面顯示了Thread.sleep()的棧幀信息,并標(biāo)記了native)
4. 方法區(qū)
該區(qū)域是線程共享的區(qū)域,用來(lái)存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。該區(qū)域在JDK1.7以前是以永久代方式實(shí)現(xiàn)的,存在于堆中,可以通過(guò)-XX:PermSize(初始值)、-XX:MaxPermSize(最大值)參數(shù)設(shè)置大小;而1.8以后以元空間方式實(shí)現(xiàn),使用的是直接內(nèi)存(但運(yùn)行時(shí)常量池和靜態(tài)變量仍放在堆中),可以通過(guò)-XX:MetaspaceSize(初始值)、-XX:MaxMetaspaceSize(最大值)控制大小,如果不設(shè)置則只受限于本地內(nèi)存大小。為什么會(huì)這么改變呢?因?yàn)榉椒▍^(qū)和堆都會(huì)進(jìn)行垃圾回收,但是方法區(qū)中的信息相對(duì)比較靜態(tài),回收難以達(dá)到成效,同時(shí)需要占用的空間大小更多的取決于我們class的大小和數(shù)量,即對(duì)該區(qū)域難以設(shè)置一個(gè)合理的大小,所以將其直接放到本地內(nèi)存中是非常有用且合理的。
在方法區(qū)中還存在常量池(1.7后放入堆中),而常量池也分了幾種,常常讓初學(xué)者比較困惑,比如靜態(tài)常量池、運(yùn)行時(shí)常量池、字符串常量池。靜態(tài)常量池就是指存在于我們的class文件中的常量池,通過(guò)javap -v ClassDemo.class反編譯上面的代碼可以看到該常量池:
Constant pool: #1 = Methodref #5.#26 // java/lang/Object."<init>":()V #2 = Class #27 // cn/dark/ClassDemo #3 = Methodref #2.#26 // cn/dark/ClassDemo."<init>":()V #4 = Methodref #2.#28 // cn/dark/ClassDemo.work:()I #5 = Class #29 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 LocalVariableTable #11 = Utf8 this #12 = Utf8 Lcn/dark/ClassDemo; #13 = Utf8 work #14 = Utf8 ()I #15 = Utf8 x #16 = Utf8 I #17 = Utf8 y #18 = Utf8 z #19 = Utf8 main #20 = Utf8 ([Ljava/lang/String;)V #21 = Utf8 args #22 = Utf8 [Ljava/lang/String; #23 = Utf8 MethodParameters #24 = Utf8 SourceFile #25 = Utf8 ClassDemo.java #26 = NameAndType #6:#7 // "<init>":()V #27 = Utf8 cn/dark/ClassDemo #28 = NameAndType #13:#14 // work:()I #29 = Utf8 java/lang/Object
靜態(tài)常量池中就是存儲(chǔ)了類和方法的信息、符號(hào)引用以及字面量等東西,當(dāng)類加載到內(nèi)存中后,JVM就會(huì)將這些內(nèi)容存放到運(yùn)行時(shí)常量池中,同時(shí)會(huì)將符號(hào)引用(可以理解為對(duì)象方法的定位描述符)會(huì)被解析為直接引用(即對(duì)象的內(nèi)存地址)存入到運(yùn)行時(shí)常量池中(因?yàn)樵陬惣虞d之前并不知道符號(hào)引用所對(duì)應(yīng)的對(duì)象內(nèi)存地址是多少,需要用符號(hào)替代)。而字符串常量池網(wǎng)上爭(zhēng)議比較多,我個(gè)人理解它也是運(yùn)行時(shí)常量池的一部分,專門用于存儲(chǔ)字符串常量,這里先簡(jiǎn)單提一下,稍后會(huì)詳細(xì)分析字符串常量池。
5. 堆
這個(gè)區(qū)域是垃圾回收的重點(diǎn)區(qū)域,對(duì)象都存在于堆中(但隨著JIT編譯器的發(fā)展和逃逸分析技術(shù)的成熟,對(duì)象也不一定都是存在于堆中),可以通過(guò)-Xms(最小值)、-Xmx(最大值)、-Xmn(新生代大小)、-XX:NewSize(新生代最小值)、-XX:MaxNewSize(新生代最大值)這些參數(shù)進(jìn)行控制。
在堆中又分為了新生代和老年代,新生代又分為Eden空間、From Survivor空間、To Survivor空間。詳細(xì)內(nèi)容后面文章會(huì)詳細(xì)講解,這里不過(guò)多闡述。
6. 直接內(nèi)存
直接內(nèi)存也叫堆外內(nèi)存,不屬于JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,主要通過(guò)DirectByteBuffer申請(qǐng)內(nèi)存,該對(duì)象存在于堆中,包含了對(duì)堆外內(nèi)存的引用;另外也可以通過(guò)Unsafe類或其它JNI手段直接申請(qǐng)內(nèi)存。它的大小受限于本地內(nèi)存的大小,也可以通過(guò)-XX:MaxDirectMemorySize設(shè)置,所以這一塊也會(huì)出現(xiàn)OOM異常且較難排查。
字符串常量池
這個(gè)區(qū)域不是虛擬機(jī)規(guī)范中的內(nèi)容,所有官方的正式文檔中也沒有明確指出有這一塊,所以這里只是根據(jù)現(xiàn)象推導(dǎo)出結(jié)論。什么現(xiàn)象呢?有一個(gè)關(guān)于字符串對(duì)象的高頻面試題:下面的代碼究竟會(huì)創(chuàng)建幾個(gè)對(duì)象?
String str = "abc"; String str1 = new string("cde");
我們先不管這個(gè)面試題,先來(lái)思考下面代碼的輸出結(jié)果是怎樣的(以下試驗(yàn)基于JDK8,更早的版本結(jié)果會(huì)有所不同):
tring s1 = "abc"; String s2 = "ab" + "c"; String s3 = new String("abc"); String s4 = new StringBuilder("ab").append("c").toString(); System.out.println("s1 == s2:" + (s1 == s2)); System.out.println("s1 == s3:" + (s1 == s3)); System.out.println("s1 == s4:" + (s1 == s4)); System.out.println("s1 == s3.intern:" + (s1 == s3.intern())); System.out.println("s1 == s4.intern:" + (s1 == s4.intern()));
輸出結(jié)果如下:
s1 == s2:true
s1 == s3:false
s1 == s4:false
s1 == s3.intern:true
s1 == s4.intern:true
上面的輸出結(jié)果和你想象的是否一樣呢?為什么呢?一個(gè)個(gè)來(lái)分析。
- s1 == s2:字面量“abc”會(huì)首先去字符串常量池找是否有"abc"這個(gè)字符串,如果有直接返回引用,如果沒有則創(chuàng)建一個(gè)新對(duì)象并返回引用;s2你可能會(huì)覺得會(huì)創(chuàng)建"ab"、"c"和“abc”三個(gè)對(duì)象,但實(shí)際上首先會(huì)被編譯器優(yōu)化為“abc”,所以等同于s1,即直接從字符串常量池返回s1的引用。
- s1 == s3:s3是通過(guò)new創(chuàng)建的,所以這個(gè)String對(duì)象肯定是存在于堆的,但是其中的char[]數(shù)組是引用字符創(chuàng)常量池中的s1,如果在這之前沒有定義的話會(huì)先在常量池中創(chuàng)建“abc”對(duì)象。所以這里可能會(huì)創(chuàng)建一個(gè)或兩個(gè)對(duì)象。
- s1 == s4:s4通過(guò)StringBuilder拼接字符串對(duì)象,所以看起來(lái)理所當(dāng)然的s1 != s4,但實(shí)際上也沒那么簡(jiǎn)單,反編譯上面的代碼會(huì)可以發(fā)現(xiàn)這里又會(huì)被編譯器優(yōu)化為s4 = "ab" + "c"。猜猜這下會(huì)創(chuàng)建幾個(gè)對(duì)象呢?拋開前面創(chuàng)建的對(duì)象的影響,這里會(huì)創(chuàng)建3個(gè)對(duì)象,因?yàn)榕cs2不同的是s4是編譯器優(yōu)化過(guò)后還存在“+”拼接,因此會(huì)在字符創(chuàng)常量池創(chuàng)建“ab”、"c"以及“abc”三個(gè)對(duì)象。前兩個(gè)可以反編譯看字節(jié)碼指令或是通過(guò)內(nèi)存搜索驗(yàn)證,而第三個(gè)的驗(yàn)證稍后進(jìn)行。
- s1 == s3.intern/s4.intern:這兩個(gè)為什么是true呢?先來(lái)看看周志明在《深入理解Java虛擬機(jī)》書中說(shuō)的:
使用String類的intern方法動(dòng)態(tài)添加字符串常量到運(yùn)行時(shí)常量池中(intern方法在1.6和1.7及以后的實(shí)現(xiàn)不相同,1.6字符串常量池放于永久代中,intern會(huì)把首次遇到的字符串實(shí)例復(fù)制永久代中并返回永久代中的引用,而1.7及以后常量池也放入到了堆中,intern也不會(huì)再?gòu)?fù)制實(shí)例,只是在常量池中記錄首次出現(xiàn)的實(shí)例引用)。
上面的意思很明確,1.7以后intern方法首先會(huì)去字符串常量池尋找對(duì)應(yīng)的字符串,如果找到了則返回對(duì)應(yīng)的引用,如果沒有找到則先會(huì)在字符串常量池中創(chuàng)建相應(yīng)的對(duì)象。因此,上面s4和s4調(diào)用intern方法時(shí)都是返回s1的引用。
看到這里,相信各位讀者基本上也都能理解了,對(duì)于開始的面試題應(yīng)該也是心中有數(shù)了,最后再來(lái)驗(yàn)證剛剛說(shuō)的“第三個(gè)對(duì)象”的問(wèn)題,先看下面代碼:
String s4 = new StringBuilder("ab").append("c").toString(); System.out.println(s4 == s4.intern());
這里結(jié)果是true。為什么呢?別急,再來(lái)看另外一段代碼:
String s3 = new String("abc"); String s4 = new StringBuilder("ab").append("c").toString(); System.out.println(s3 == s3.intern()); System.out.println(s4 == s4.intern());
這里結(jié)果是兩個(gè)false,和你心中的答案是一致的么?上文剛剛說(shuō)了intern會(huì)先去字符串常量池找,找到則返回引用,否則在字符創(chuàng)常量池創(chuàng)建一個(gè)對(duì)象,所以第一段代碼結(jié)果等于true正好說(shuō)明了通過(guò)StringBuilder拼接的字符串會(huì)存到字符串常量池中;而第二段代碼中,在StringBuilder拼接字符串之前已經(jīng)優(yōu)先使用new創(chuàng)建了字符串,也就會(huì)在字符串常量里創(chuàng)建“abc”對(duì)象,因此s4.intern返回的是該常量的引用,和s4不相等。你可能會(huì)說(shuō)是因?yàn)閮?yōu)先調(diào)用了s3.intern方法,但即使你去掉這一段,結(jié)果還是一樣的,也剛好驗(yàn)證了new String("abc")會(huì)創(chuàng)建兩個(gè)對(duì)象(在此之前沒有定義“abc”字面量,就會(huì)在字符串常量池創(chuàng)建對(duì)象,然后堆中創(chuàng)建String對(duì)象并引用該常量,否則只會(huì)創(chuàng)建堆中的String對(duì)象)。
總結(jié)
本文是JVM系列的開篇,主要分析JVM的運(yùn)行時(shí)數(shù)據(jù)區(qū)、簡(jiǎn)單參數(shù)設(shè)置和字節(jié)碼閱讀分析,這也是學(xué)習(xí)JVM及性能調(diào)優(yōu)的基礎(chǔ),讀者需要深刻理解這些內(nèi)容以及哪些區(qū)域會(huì)發(fā)生內(nèi)存溢出(只有程序計(jì)數(shù)器不會(huì)內(nèi)存溢出),另外關(guān)于運(yùn)行時(shí)常量池和字符串常量池的內(nèi)容也需要理解透徹。
到此這篇關(guān)于深入探究JVM之內(nèi)存結(jié)構(gòu)及字符串常量池的文章就介紹到這了,更多相關(guān)JVM內(nèi)存結(jié)構(gòu)字符串常量池內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Java 反射和反射的應(yīng)用場(chǎng)景
這篇文章主要介紹了Java 反射和反射的應(yīng)用場(chǎng)景的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)Java反射的相關(guān)知識(shí),感興趣的朋友可以了解下2020-08-08SpringBoot項(xiàng)目整合MybatisPlus并使用SQLite作為數(shù)據(jù)庫(kù)的過(guò)程
SQLite是一個(gè)緊湊的庫(kù),啟用所有功能后,庫(kù)大小可以小于 750KiB, 具體取決于目標(biāo)平臺(tái)和編譯器優(yōu)化設(shè)置, 內(nèi)存使用量和速度之間需要權(quán)衡,這篇文章主要介紹了SpringBoot項(xiàng)目整合MybatisPlus并使用SQLite作為數(shù)據(jù)庫(kù),需要的朋友可以參考下2024-07-07Java 數(shù)組獲取最大和最小值的實(shí)例實(shí)現(xiàn)
這篇文章主要介紹了Java 數(shù)組獲取最大和最小值的實(shí)例實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09mybatis參數(shù)String與Integer類型的判斷方式
這篇文章主要介紹了mybatis參數(shù)String與Integer類型的判斷方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03Java實(shí)戰(zhàn)之晚會(huì)抽獎(jiǎng)系統(tǒng)的實(shí)現(xiàn)
這篇文章主要介紹了如何利用Java語(yǔ)言編寫一個(gè)晚會(huì)抽獎(jiǎng)系統(tǒng),文中采用到的技術(shù)有Jdbc、Servlert、JavaScript、JQuery、Ajax等,感興趣的可以學(xué)習(xí)一下2022-03-03