java虛擬機之內存管理

     俗話說,不想當將軍的士兵不是好士兵,作爲一個有追求的java程序猿,終極目標必然是系統架構師(捂臉)~而要想成爲一名合格的系統架構師,一定要對JVM有深入的研究與理解。建議對JVM感興趣的朋友可以讀一讀周志明編寫的《深入理解java虛擬機》一書,本文大多內容即出自對該書的摘抄與整理。

     1995年5月23日,SunWorld大會上正式發佈了java1.0的版本,java語言第一次提出“Write Once,Run Anywhere”的口號。如今,距離java語言第一次發佈已經將近二十年的時間,java也不斷地在發展,每次升級都伴隨着新特性的發佈。一般來說,我們把java程序設計語言、java虛擬機、java API類庫這三部分統稱爲JDK(Java Development Kit)。JDK是支持java程序開發的最小環境,另外,可以把Java API類庫中的Java SE API子集和Java虛擬機兩部分統稱爲JRE(Java Runtime Environment),JRE是支持Java程序運行的標準環境。而JDK除了SUN公司發佈的標準JDK之外,還有一些開源的JDK,比如Sun在2006年把Java源碼開放而形成的OpenJDK。

     我們都知道,使用Java語言不用每次new一個對象之後去寫配對的delete/free代碼,不容易造成內存泄露和內存溢出問題,因爲內存都由Java虛擬機來管理,JVM自帶的垃圾回收機制會定期按照一定的算法(後面會談到)回收不再使用的對象並釋放對應的內存。Java虛擬機在執行Java程序的過程中會把管理的內存區域劃分爲若干個不同的數據區域,1.程序計數器 2.java虛擬機棧 3.本地方法棧 4.java堆 5.方法區。如下圖所示:

                                                                      

                                            

     順便提一下java的類加載機制,當我們在命令行執行java  xxx.class命令的時候,java.exe首先找到JRE,然後找到位於JRE之中的JVM.dll(真正的JAVA虛擬機),最後加載這個動態聯結函式庫,啓動java虛擬機。虛擬機啓動後首先進行初始化,初始化完成後生成第一個類加載器BootstrapLoader。BootstrapLoader是由C++編寫而成的,所以以java的觀點來看邏輯上並不存在BootstrapLoader的類別實體,因此在試圖通過getClass().getClassLoader()來打印時會打印出null。BootstrapLoader生成後會載入定義在sun.misc命名空間下的Launcher.java之中的ExtClassLoader。ExtClassLoader是一個內部類,編譯之後會變成Launcher$ExtClassLoader.class,它的父加載器爲BootstrapLoader。然後BootstrapLoader再加載Launcher.java之中的AppClassLoader(和ExtClassLoader一樣也是一個內部類),它的parent爲ExtClassLoader實體。這裏ExtClassLoader與AppClassLoader都是由BootstrapLoader加載的,因此parent是誰和由哪個類別加載器加載並沒有關係。他們的關係如下圖所示:

                                     

最後由AppclassLoader加載程序員編譯之後的.class文件。

    下面回來繼續看虛擬機所管理的幾個運行時數據區域:

1.程序計數器

    程序計數器(Program Counter Register)是一塊較小的內存空間,可以看做是當前線程所執行的字節碼的行號指示器。由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,因此爲了線程切換後能回到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響獨立存儲,因此程序計數器這部分內存區域爲線程私有的內存。該內存區域也是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

2. Java虛擬機棧

    Java虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表,操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

    局部變量表存放了編譯期可知的各種基本數據類型、對象引用和returnAddress類型,它所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

    Java虛擬機規範中,對Java虛擬機棧規定了兩種異常情況:當線程請求的棧深度大於虛擬機所允許的深度時將拋出StackOverFlowError異常;如果虛擬機棧動態擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。

3. 本地方法棧

    本地方法棧(Native Method Stack)與虛擬機棧的作用非常相似,不同的是虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則爲虛擬機使用到的Native方法服務。與虛擬機棧一樣,本地方法棧也會拋出StackOverFlowError和OutOfMemoryError異常。

4. Java堆

    對大多數應用程序來說,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊,它是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此區域主要用來存放對象實例,幾乎所有的對象實例都在這裏分配內存。

    Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱爲GC堆。由於現在的垃圾收集器基本都採用分代收集算法,所以Java堆還可以細分爲新生代和老年代,而新生代又可以分爲Eden空間、From Survivor空間和To Survivor空間。Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,可以通過-Xmx和-Xms控制Java堆的大小。如果在堆中沒有足夠的內存完成實例分配,並且堆也無法擴展時就會拋出OutOfMemoryError異常。

5. 方法區

    方法區(Method Area)和Java堆一樣,是各個線程共享的內存區域,用於存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的的代碼等數據。Java虛擬機規範把方法區描述爲Java堆的一個邏輯部分,對於HotSpot虛擬機來說,GC分代收集也可以在方法區上進行,因此也把方法區成爲“永久代”(Permanent Generation)。當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

    運行時常量池(Runtime Constant Pool)是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

    運行時常量池相對於Class文件常量池的另一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是說除了預置入Class文件常量池中的內容外,運行期間也可能將新的常量放入方法區運行時常量池。如String的inter()方法。

6. 直接內存

    這裏還要說明一下直接內存(Direct Memory)的概念,它並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,但是這部分內存也經常被頻繁使用,並且也可能會導致OutofMemoryError異常。

    JDK1.4之後引入了NIO(New Input/Output)類,相對於以前同步阻塞的OIO來說,可以執行異步非阻塞的IO操作,NIO引入了一種基於通道Channel和緩衝區Buffer的I/O方式,可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。當各個內存區域綜合大於物理內存限制時,就會使動態擴展時出現OutOfMemoryError異常。

    以上幾個部分就是虛擬機所管理的運行時內存區域了,那麼當一個對象被創建時,虛擬機都做了哪些工作呢?虛擬機遇到一條new指令時,會首先檢查這個指令的參數是否能在常量池中定位到一個類的符號的引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過,如果沒有則執行類加載的過程。在類加載檢查通過後,虛擬機將爲新生對象分配內存。由於對象所需的內存大小在類加載完成後已經確定了,因此爲對象分配內存空間只需要把一塊確定大小的內存從Java堆中劃分出來。如果Java堆內存空間是絕對規整的,已用內存和空閒內存分放兩邊,那麼可以使用“指針碰撞”的分配方式。如果內存不規整,已使用的內存和未使用的內存相互交錯,則需要使用“空閒列表”的分配方式,由虛擬機維護一個列表,記錄那些內存塊是可用的,分配的時候在列表上找到一塊足夠大的空間分配給對象實例,同時更新列表記錄。內存分配完成之後,虛擬機需要將分配到的內存空間都初始化爲零(不包括對象頭),接下來虛擬機還要對對象進行一些必要的設置,如對象是哪個類的實例、如何找到類的元數據信息、對象的哈希嗎、對象的GC分代年齡等信息。這些信息存放在對象的對象頭中。在上面工作都完成之後,對於虛擬機而言,一個新的對象就產生了。

    



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章