高手支招 Java經(jīng)驗(yàn)分享(十二)
作為一個(gè)Java程序員,如果不了解JVM的工作原理,就很難從底層去把握J(rèn)ava語(yǔ)言和Java程序的運(yùn)作機(jī)制。這里先推薦一個(gè)最權(quán)威的講解JVM的文檔,大家只要查過(guò)Java API的可以在里面的一個(gè)叫“API, Language, and Virtual Machine Document”的標(biāo)題下看到四個(gè)子標(biāo)題,***個(gè)是我們最熟悉的Java API Specification,很少會(huì)有人注意到第三和第四個(gè)子標(biāo)題,分別是“The Java Language Specification”和“The Java Machine Specification”后面都帶有(Download)字樣,JVM的那個(gè)URL直接鏈接到http://java.sun.com/docs/books/vmspec/2nd-edition/這里地址。我們可以下載到一份非常權(quán)威詳細(xì)的講解JVM原理的官方文檔。筆者業(yè)余時(shí)間花了1個(gè)星期來(lái)閱讀,這里把自己的收獲跟大家來(lái)分享一下,大概從這么幾個(gè)方面來(lái)談一談:
1. JVM的實(shí)現(xiàn)機(jī)制
Java虛擬機(jī)就是一個(gè)小的計(jì)算機(jī),有自己的指令集,有自己的文件系統(tǒng),管理內(nèi)部的表和數(shù)據(jù),負(fù)責(zé)讀取class文件里面字節(jié)碼,然后轉(zhuǎn)換成不同操作系統(tǒng)的CPU指令,從而使得Java程序在不同的操作系統(tǒng)上順利的跑起來(lái)。所以Window的JVM能把字節(jié)碼轉(zhuǎn)換成Window系統(tǒng)的指令集,Linux的JVM能把字節(jié)碼轉(zhuǎn)換成Linux系統(tǒng)的字節(jié),同理還有Solaris,它們彼此之間是不能通用的。最早一款的原型雖然是Sun公司開(kāi)發(fā)的,但發(fā)展到現(xiàn)在其實(shí)任何廠商都可以自己去實(shí)現(xiàn)一個(gè)虛擬機(jī),用來(lái)讀取字節(jié)碼轉(zhuǎn)換成OS指令。甚至我們可以認(rèn)為JVM跟Java編程語(yǔ)言都沒(méi)有關(guān)系,因?yàn)槟阕约耗呐掠糜浭卤緦?xiě)一串字節(jié)碼,也可以讓JVM來(lái)解析運(yùn)行,只要你的字節(jié)碼能通過(guò)JVM的驗(yàn)證。
JVM的驗(yàn)證其實(shí)是很?chē)?yán)格的,這里只講一些有趣的地方。大家還記得Java的圖標(biāo)是一個(gè)杯咖啡麼?究其歷史我們也許可以查出為什么,但還有更顯而易見(jiàn)的方式是JVM怎么判斷一個(gè)文件是否是class文件?JVM的做法是讀取前4個(gè)字節(jié)轉(zhuǎn)換成16進(jìn)制數(shù),判斷是否等于0xCAFEBABE這個(gè)數(shù)。注意到這個(gè)單詞了麼?“cafebabe”,代表著國(guó)外一種咖啡品牌,似乎叫做Peet’s coffee-baristas之類(lèi)。創(chuàng)造Java的人為了方便記憶,選擇了這樣一個(gè)16進(jìn)制數(shù)作為標(biāo)準(zhǔn)class文件的頭,所以任何class文件都必須具有這4個(gè)字節(jié)的頭部。我們可以用DataInput這個(gè)接口的實(shí)現(xiàn)類(lèi)來(lái)驗(yàn)證一下,讀取任何一個(gè)class文件的***個(gè)int,int在Java里面是四個(gè)字節(jié)。轉(zhuǎn)換成16進(jìn)制一定會(huì)是0xcafebabe的。
所以這里想告訴大家的是,JVM其實(shí)并沒(méi)有那么神秘,我們完全可以理解它的構(gòu)造。
2. Java相關(guān)的基礎(chǔ)概念
配合JVM的結(jié)構(gòu),在Java語(yǔ)言中也會(huì)有很多特點(diǎn)比較鮮明的地方。比如對(duì)數(shù)值計(jì)算從來(lái)不會(huì)檢查位溢出。任何變量存儲(chǔ)的二進(jìn)制即使位全部為1了仍然可以加,全部為0了仍然可以減。大家只要稍微測(cè)試一下就知道了,看這幾個(gè)例子:
- int max = Integer.MAX_VALUE;
 - int min = Integer.MIN_VALUE;
 - max+1 == min; //true
 - min-1 == max; //true
 - 0.0/0.0 //得到“NaN”(Not a number)
 - 1/0.0 //Infinity
 - -1/0.0 //-Infinity
 - 1或-1/0 //ArithmeticException唯一的異常情況
 
看完這幾個(gè)例子,大家是否能更好的把握J(rèn)ava的數(shù)值運(yùn)算呢?Java完全遵照IEEE-754的標(biāo)準(zhǔn)來(lái)定義單雙精度浮點(diǎn)數(shù)以及其他的數(shù)值存儲(chǔ)方式。
另外Java里面有一個(gè)概念叫做Daemon Thread(守護(hù)線程),知道它的存在主要是為了理解虛擬機(jī)的生命周期。當(dāng)我們運(yùn)行java命令,從main函數(shù)進(jìn)入的那一刻起,虛擬機(jī)就開(kāi)始啟動(dòng)運(yùn)行了。Main所在的主線程也會(huì)啟動(dòng)起來(lái),它屬于非守護(hù)線程。與之同時(shí)一些守護(hù)線程也會(huì)同時(shí)啟動(dòng),最典型的守護(hù)線程代表就是GC(垃圾收集器)線程。JVM虛擬機(jī)什么時(shí)候退出呢?是在所有的非守護(hù)線程結(jié)束的那一刻,JVM就exit。注意這個(gè)時(shí)候守護(hù)線程并未退出,很可能還要繼續(xù)完成它的本職工作之后才會(huì)結(jié)束,但虛擬機(jī)的生命周期已經(jīng)提前于它結(jié)束了。
3. JVM內(nèi)部的基本概念
虛擬機(jī)內(nèi)部還有一些概念,全部列舉是不現(xiàn)實(shí)的,太繁瑣也沒(méi)有意義。除非您真的想自己去做一個(gè)JVM。筆者只列舉部分概念:
首先我們來(lái)看一個(gè)叫做ReturnAddress的變量,它是JVM用來(lái)存儲(chǔ)方法出口或者說(shuō)進(jìn)行跳轉(zhuǎn)的依據(jù),把任何地址存入這個(gè)變量就一定會(huì)按照這個(gè)地址來(lái)跳轉(zhuǎn)。我們需要注意的就是finally有比方法return更高的賦值給ReturnAddress的優(yōu)先級(jí)。同時(shí)存在方法return和finally return的話,一定是按照f(shuō)inally里面的return為準(zhǔn)。
JVM有自己的Heap,能被所有線程共享,存儲(chǔ)著所有的對(duì)象,內(nèi)存是動(dòng)態(tài)被分配的。對(duì)于每個(gè)線程,擁有自己的Stack,棧里面存儲(chǔ)的單位叫做Frame(楨)。楨里面就記錄著零時(shí)變量、對(duì)象引用地址、方法返回值等數(shù)據(jù)。JVM還有一個(gè)叫做Method Area的地方,存儲(chǔ)著一段一段的可執(zhí)行代碼,每一段就是一個(gè)方法體,也能被所有線程共享。所以我們說(shuō)一個(gè)線程其實(shí)從run方法跑起來(lái),跟它的類(lèi)中聲明的其他方法是兩個(gè)概念。因?yàn)槠渌姆椒òǖ乃械膶?duì)象,這個(gè)時(shí)候都充當(dāng)為資源被線程使用。
JVM有自己管理內(nèi)存的方案,因?yàn)樗哂形募到y(tǒng)的功能,我們可以看成一個(gè)小型的數(shù)據(jù)庫(kù),內(nèi)部有許許多多不同的表。表的字段可能是另外一張表的地址,也可以直接就是一個(gè)存儲(chǔ)數(shù)據(jù)值的地址值。JVM所有對(duì)運(yùn)行時(shí)候類(lèi)的解析驗(yàn)證計(jì)算等管理工作,實(shí)際上都是在管理這些表的變動(dòng),如果我們從數(shù)據(jù)庫(kù)的角度來(lái)看,JVM所做的就是根據(jù)你的代碼來(lái)操作那么多個(gè)表***返回給你結(jié)果的過(guò)程。里面的表結(jié)構(gòu)包括class的表、field表、method表、attribute表等。
4. JVM的指令集
JVM有自己的指令集,筆者從前也看過(guò)一些計(jì)算機(jī)組成結(jié)構(gòu)和匯編語(yǔ)言的數(shù),建議大家也稍微看看,了解設(shè)計(jì)一個(gè)高效可用的計(jì)算機(jī)指令集是多么復(fù)雜又多么重要的過(guò)程。對(duì)于JVM的指令集,職責(zé)是管理好Java程序編譯出來(lái)的字節(jié)碼,相對(duì)而言指令集的名稱就多少和Java語(yǔ)言相關(guān)了,比如指令集里就有sastore,、saload表示array里面short的存和取、類(lèi)似還有d2i表示從double轉(zhuǎn)換成int、monitorenter表示進(jìn)入synchronized塊加鎖、getstatic和putstatic表示對(duì)靜態(tài)標(biāo)量的存取、 jsr和ret等跳轉(zhuǎn)指令……
為了便于記憶,設(shè)計(jì)JVM指令集的人們約定f開(kāi)頭的跟float有關(guān),d跟double有關(guān),i跟int有關(guān),s跟short有關(guān),a跟array有關(guān)。有興趣的可以細(xì)讀文檔里面的每一個(gè)指令的作用。因?yàn)橹皇亲鳛槌醪搅私?,這里就不多說(shuō)了。
5. 一些Java關(guān)鍵字的實(shí)現(xiàn)原理
文檔還很詳細(xì)的列舉了很多加載、初始化、加鎖等操作的過(guò)程。筆者覺(jué)得比較有用的***是記住Java里面只有Array不是由ClassLoader加載的對(duì)象,其他的對(duì)象全部都必須由一個(gè)ClassLoader來(lái)加載。另外package的概念除了類(lèi)似于C++的namespace,是一種命名空間之外,底層的實(shí)現(xiàn)是規(guī)定同一個(gè)package下的類(lèi)必須由同一個(gè)類(lèi)加載器來(lái)加載,所以package的概念還可以認(rèn)為是被同一個(gè)類(lèi)加載器加載的類(lèi)。
另外在多線程中,有很多細(xì)節(jié)值得去體會(huì)。每個(gè)線程有自己的Working memory,它們從能被共享的Main Memory中去讀數(shù)據(jù)、修改、然后再存回去。筆者一直認(rèn)為線程就是數(shù)據(jù)庫(kù)里面事務(wù)的前身或者說(shuō)祖先。我們只要稍微比較一下它們的行為,就會(huì)發(fā)現(xiàn)很多一致性。事務(wù)也是操作被事務(wù)共享的表數(shù)據(jù),你改完我改,順序不一致就會(huì)出現(xiàn)臟數(shù)據(jù),而線程同樣會(huì)出現(xiàn)臟數(shù)據(jù)。我們對(duì)線程加的鎖策略,同樣在事務(wù)中也有適用。當(dāng)然多事務(wù)的情況顯然比多線程更加復(fù)雜,但我們只要理解了多線程,相信對(duì)學(xué)習(xí)數(shù)據(jù)庫(kù)事務(wù)的效果也是非常有幫助的。Java里面除了synchronized能夠幫助同步多線程之外,還有一個(gè)弱同步的操作關(guān)鍵字是volatile,它產(chǎn)生在變量上的約束在文檔中也有詳細(xì)的說(shuō)明。因?yàn)楹軓?fù)雜,考慮到篇幅筆者就不打算解釋一遍了。
好了,又是新的一篇結(jié)束了。不足之處大家盡管提出來(lái),筆者愿意接受各種職責(zé)批評(píng)。這個(gè)帖子一直以來(lái)得到那么多朋友的大力支持和鼓勵(lì),筆者在這里真誠(chéng)的說(shuō)一聲謝謝!
【編輯推薦】















 
 
 
 
 
 
 