JVM是我們Javaer的最基本功底了,剛開始學Java的時候,一般都是從“Hello World”開始的,然后會寫個復雜點class,然后再找一些開源框架,比如Spring,Hibernate等等,再然后就開發(fā)企業(yè)級的應用,比如網(wǎng)站、企業(yè)內部應用、實時交易系統(tǒng)等等,直到某一天突然發(fā)現(xiàn)做的系統(tǒng)咋就這么慢呢,而且時不時還來個內存溢出什么的,今天是交易系統(tǒng)報了StackOverflowError,明天是網(wǎng)站系統(tǒng)報了個OutOfMemoryError,這種錯誤又很難重現(xiàn),只有分析Javacore和dump文件,運氣好點還能分析出個結果,運行遭的點,就直接去廟里燒香吧!每天接客戶的電話都是戰(zhàn)戰(zhàn)兢兢的,生怕再出什么幺蛾子了。我想Java做的久一點的都有這樣的經(jīng)歷,那這些問題的最終根結是在哪呢?—— JVM。
JVM全稱是Java Virtual Machine,Java虛擬機,也就是在計算機上再虛擬一個計算機,這和我們使用 VMWare不一樣,那個虛擬的東西你是可以看到的,這個JVM你是看不到的,它存在內存中。我們知道計算機的基本構成是:運算器、控制器、存儲器、輸入和輸出設備,那這個JVM也是有這成套的元素,運算器是當然是交給硬件CPU還處理了,只是為了適應“一次編譯,隨處運行”的情況,需要做一個翻譯動作,于是就用了JVM自己的命令集,這與匯編的命令集有點類似,每一種匯編命令集針對一個系列的CPU,比如8086系列的匯編也是可以用在8088上的,但是就不能跑在8051上,而JVM的命令集則是可以到處運行的,因為JVM做了翻譯,根據(jù)不同的CPU,翻譯成不同的機器語言。
JVM中我們最需要深入理解的就是它的存儲部分,存儲?硬盤?NO,NO, JVM是一個內存中的虛擬機,那它的存儲就是內存了,我們寫的所有類、常量、變量、方法都在內存中,這決定著我們程序運行的是否健壯、是否高效,接下來的部分就是重點介紹之。
我們先把JVM這個虛擬機畫出來,如下圖所示:
從這個圖中可以看到,JVM是運行在操作系統(tǒng)之上的,它與硬件沒有直接的交互。我們再來看下JVM有哪些組成部分,如下圖所示:
q Class Loader 類加載器
類加載器的作用是加載類文件到內存,比如編寫一個HelloWord.java程序,然后通過javac編譯成class文件,那怎么才能加載到內存中被執(zhí)行呢?Class Loader承擔的就是這個責任,那不可能隨便建立一個.class文件就能被加載的,Class Loader加載的class文件是有格式要求,在《JVM Specification》中式這樣定義Class文件的結構:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
需要詳細了解的話,可以仔細閱讀《JVM Specification》的第四章“The class File Format”,這里不再詳細說明。
友情提示:Class Loader只管加載,只要符合文件結構就加載,至于說能不能運行,則不是它負責的,那是由Execution Engine負責的。
q Execution Engine 執(zhí)行引擎
執(zhí)行引擎也叫做解釋器(Interpreter),負責解釋命令,提交操作系統(tǒng)執(zhí)行。
q Native Interface本地接口
本地接口的作用是融合不同的編程語言為Java所用,它的初衷是融合C/C++程序,Java誕生的時候是C/C++橫行的時候,要想立足,必須有一個聰明的、睿智的調用C/C++程序,于是就在內存中專門開辟了一塊區(qū)域處理標記為native的代碼,它的具體做法是Native Method Stack中登記native方法,在Execution Engine執(zhí)行時加載native libraies。目前該方法使用的是越來越少了,除非是與硬件有關的應用,比如通過Java程序驅動打印機,或者Java系統(tǒng)管理生產(chǎn)設備,在企業(yè)級應用中已經(jīng)比較少見,因為現(xiàn)在的異構領域間的通信很發(fā)達,比如可以使用Socket通信,也可以使用Web Service等等,不多做介紹。
q Runtime data area運行數(shù)據(jù)區(qū)
運行數(shù)據(jù)區(qū)是整個JVM的重點。我們所有寫的程序都被加載到這里,之后才開始運行,Java生態(tài)系統(tǒng)如此的繁榮,得益于該區(qū)域的優(yōu)良自治,下一章節(jié)詳細介紹之。
整個JVM框架由加載器加載文件,然后執(zhí)行器在內存中處理數(shù)據(jù),需要與異構系統(tǒng)交互是可以通過本地接口進行,瞧,一個完整的系統(tǒng)誕生了!
所有的數(shù)據(jù)和程序都是在運行數(shù)據(jù)區(qū)存放,它包括以下幾部分:
q Stack 棧
棧也叫棧內存,是Java程序的運行區(qū),是在線程創(chuàng)建時創(chuàng)建,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對于棧來說不存在垃圾回收問題,只要線程一結束,該棧就Over。問題出來了:棧中存的是那些數(shù)據(jù)呢?又什么是格式呢?
棧中的數(shù)據(jù)都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區(qū)塊,是一個數(shù)據(jù)集,是一個有關方法(Method)和運行期數(shù)據(jù)的數(shù)據(jù)集,當一個方法A被調用時就產(chǎn)生了一個棧幀F1,并被壓入到棧中,A方法又調用了B方法,于是產(chǎn)生棧幀F2也被壓入棧,執(zhí)行完畢后,先彈出F2棧幀,再彈出F1棧幀,遵循“先進后出”原則。
那棧幀中到底存在著什么數(shù)據(jù)呢?棧幀中主要保存3類數(shù)據(jù):本地變量(Local Variables),包括輸入?yún)?shù)和輸出參數(shù)以及方法內的變量;棧操作(Operand Stack),記錄出棧、入棧的操作;棧幀數(shù)據(jù)(Frame Data),包括類文件、方法等等。光說比較枯燥,我們畫個圖來理解一下Java棧,如下圖所示:
q Heap 堆內存
一個JVM實例只存在一個堆類存,堆內存的大小是可以調節(jié)的。類加載器讀取了類文件后,需要把類、方法、常變量放到堆內存中,以方便執(zhí)行器執(zhí)行,堆內存分為三部分:
Permanent Space 永久存儲區(qū)
永久存儲區(qū)是一個常駐內存區(qū)域,用于存放JDK自身所攜帶的Class,Interface的元數(shù)據(jù),也就是說它存儲的是運行環(huán)境必須的類信息,被裝載進此區(qū)域的數(shù)據(jù)是不會被垃圾回收器回收掉的,關閉JVM才會釋放此區(qū)域所占用的內存。
Young Generation Space 新生區(qū)
新生區(qū)是類的誕生、成長、消亡的區(qū)域,一個類在這里產(chǎn)生,應用,最后被垃圾回收器收集,結束生命。新生區(qū)又分為兩部分:伊甸區(qū)(Eden space)和幸存者區(qū)(Survivor pace),所有的類都是在伊甸區(qū)被new出來的。幸存區(qū)有兩個: 0區(qū)(Survivor 0 space)和1區(qū)(Survivor 1 space)。當伊甸園的空間用完時,程序又需要創(chuàng)建對象,JVM的垃圾回收器將對伊甸園區(qū)進行垃圾回收,將伊甸園區(qū)中的不再被其他對象所引用的對象進行銷毀。然后將伊甸園中的剩余對象移動到幸存0區(qū)。若幸存0區(qū)也滿了,再對該區(qū)進行垃圾回收,然后移動到1區(qū)。那如果1區(qū)也滿了呢?再移動到養(yǎng)老區(qū)。
Tenure generation space養(yǎng)老區(qū)
養(yǎng)老區(qū)用于保存從新生區(qū)篩選出來的JAVA對象,一般池對象都在這個區(qū)域活躍。 三個區(qū)的示意圖如下:
方法區(qū)是被所有線程共享,該區(qū)域保存所有字段和方法字節(jié)碼,以及一些特殊方法如構造函數(shù),接口代碼也在此定義。
q PC Register 程序計數(shù)器
每個線程都有一個程序計數(shù)器,就是一個指針,指向方法區(qū)中的方法字節(jié)碼,由執(zhí)行引擎讀取下一條指令。
q Native Method Stack 本地方法棧
問:堆和棧有什么區(qū)別
答:堆是存放對象的,但是對象內的臨時變量是存在棧內存中,如例子中的methodVar是在運行期存放到棧中的。
棧是跟隨線程的,有線程就有棧,堆是跟隨JVM的,有JVM就有堆內存。
問:堆內存中到底存在著什么東西?
答:對象,包括對象變量以及對象方法。
問:類變量和實例變量有什么區(qū)別?
答:靜態(tài)變量是類變量,非靜態(tài)變量是實例變量,直白的說,有static修飾的變量是靜態(tài)變量,沒有static修飾的變量是實例變量。靜態(tài)變量存在方法區(qū)中,實例變量存在堆內存中。
問:我聽說類變量是在JVM啟動時就初始化好的,和你這說的不同呀!
答:那你是道聽途說,信我的,沒錯。
問:Java的方法(函數(shù))到底是傳值還是傳址?
答:都不是,是以傳值的方式傳遞地址,具體的說原生數(shù)據(jù)類型傳遞的值,引用類型傳遞的地址。對于原始數(shù)據(jù)類型,JVM的處理方法是從Method Area或Heap中拷貝到Stack,然后運行frame中的方法,運行完畢后再把變量指拷貝回去。
問:為什么會產(chǎn)生OutOfMemory產(chǎn)生?
答:一句話:Heap內存中沒有足夠的可用內存了。這句話要好好理解,不是說Heap沒有內存了,是說新申請內存的對象大于Heap空閑內存,比如現(xiàn)在Heap還空閑1M,但是新申請的內存需要1.1M,于是就會報OutOfMemory了,可能以后的對象申請的內存都只要0.9M,于是就只出現(xiàn)一次OutOfMemory,GC也正常了,看起來像偶發(fā)事件,就是這么回事。 但如果此時GC沒有回收就會產(chǎn)生掛起情況,系統(tǒng)不響應了。
問:我產(chǎn)生的對象不多呀,為什么還會產(chǎn)生OutOfMemory?
答:你繼承層次忒多了,Heap中 產(chǎn)生的對象是先產(chǎn)生 父類,然后才產(chǎn)生子類,明白不?
問:OutOfMemory錯誤分幾種?
答:分兩種,分別是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen space”,兩種都是內存溢出,heap size是說申請不到新的內存了,這個很常見,檢查應用或調整堆內存大小。
“PermGen space”是因為永久存儲區(qū)滿了,這個也很常見,一般在熱發(fā)布的環(huán)境中出現(xiàn),是因為每次發(fā)布應用系統(tǒng)都不重啟,久而久之永久存儲區(qū)中的死對象太多導致新對象無法申請內存,一般重新啟動一下即可。
問:為什么會產(chǎn)生StackOverflowError?
答:因為一個線程把Stack內存全部耗盡了,一般是遞歸函數(shù)造成的。
問:一個機器上可以看多個JVM嗎?JVM之間可以互訪嗎?
答:可以多個JVM,只要機器承受得了。JVM之間是不可以互訪,你不能在A-JVM中訪問B-JVM的Heap內存,這是不可能的。在以前老版本的JVM中,會出現(xiàn)A-JVM Crack后影響到B-JVM,現(xiàn)在版本非常少見。
問:為什么Java要采用垃圾回收機制,而不采用C/C++的顯式內存管理?
答:為了簡單,內存管理不是每個程序員都能折騰好的。
問:為什么你沒有詳細介紹垃圾回收機制?
答:垃圾回收機制每個JVM都不同,JVM Specification只是定義了要自動釋放內存,也就是說它只定義了垃圾回收的抽象方法,具體怎么實現(xiàn)各個廠商都不同,算法各異,這東西實在沒必要深入。
問:JVM中到底哪些區(qū)域是共享的?哪些是私有的?
答:Heap和Method Area是共享的,其他都是私有的,
問:什么是JIT,你怎么沒說?
答:JIT是指Just In Time,有的文檔把JIT作為JVM的一個部件來介紹,有的是作為執(zhí)行引擎的一部分來介紹,這都能理解。Java剛誕生的時候是一個解釋性語言,別噓,即使編譯成了字節(jié)碼(byte code)也是針對JVM的,它需要再次翻譯成原生代碼(native code)才能被機器執(zhí)行,于是效率的擔憂就提出來了。Sun為了解決該問題提出了一套新的機制,好,你想編譯成原生代碼,沒問題,我在JVM上提供一個工具,把字節(jié)碼編譯成原生碼,下次你來訪問的時候直接訪問原生碼就成了,于是JIT就誕生了,就這么回事。
問:JVM還有哪些部分是你沒有提到的?
答:JVM是一個異常復雜的東西,寫一本磚頭書都不為過,還有幾個要說明的:
常量池(constant pool):按照順序存放程序中的常量,并且進行索引編號的區(qū)域。比如int i =100,這個100就放在常量池中。
安全管理器(Security Manager):提供Java運行期的安全控制,防止惡意攻擊,比如指定讀取文件,寫入文件權限,網(wǎng)絡訪問,創(chuàng)建進程等等,Class Loader在Security Manager認證通過后才能加載class文件的。
方法索引表(Methods table),記錄的是每個method的地址信息,Stack和Heap中的地址指針其實是指向Methods table地址。
問:為什么不建議在程序中顯式的生命System.gc()?
答:因為顯式聲明是做堆內存全掃描,也就是Full GC,是需要停止所有的活動的(Stop The World Collection),你的應用能承受這個嗎?
問:JVM有哪些調整參數(shù)?
答:非常多,自己去找,堆內存、棧內存的大小都可以定義,甚至是堆內存的三個部分、新生代的各個比例都能調整。