JAVA能夠?qū)崿F(xiàn)跨平臺的一個根本原因,是定義了class文件的格式標準,凡是實現(xiàn)該標準的JVM都能夠加載并解釋該class文件,據(jù)此也可以知道,為啥Java語言的執(zhí)行速度比C/C 語言執(zhí)行的速度要慢了,當然原因肯定不止這一個,如在JVM中沒有數(shù)據(jù)寄存器,指令集使用的是棧來保存中間數(shù)據(jù)…等,盡管Java的貢獻者們?yōu)閳?zhí)行速度的提高想了各種辦法,如JIT、動態(tài)編譯器等,以下是Leetcode中一道題目用不同的語言實現(xiàn)時的執(zhí)行性能對比圖… 以下是JVM的一個基本架構圖,在這個基本架構圖中,棧有兩部份,Java線程棧以及本地方法棧,棧的概念與C/C 程序基本上都是一個概念,里面存放的都是棧幀,一個棧幀代表的就是一個函數(shù)的調(diào)用,在棧幀里面存放了函數(shù)的形參,函數(shù)的局部變量, 返回地址等,但是與C/C 的一個重要區(qū)別是,C/C 里面有傳值以及傳址的區(qū)別,當傳的是一個對象時( 結構體也可以當成對象,其實就是對象~,只不過里面的方法默認都是public的,不信你可以試試,在結構體中加一個函數(shù),編譯器也不會報錯,程序依舊運行~~~),會將對象復到到棧中,而Java中只有基本類型才是傳值的,其他類型傳的都是引用,什么是引用,學過C/C 的就把引用當作指針理解吧~~~,在這個基本架構圖中,可以看出JVM還定義了一個本地方法棧,本地方法棧是為Java調(diào)用本地方法【這些本地方法是由其他語言編寫的】服務的 上面的圖中看到的是JVM中棧有兩個,但是堆只有一個,每一個線程都有自已的線程棧【線程棧的大小可以通過設置JVM的-xss參數(shù)進行配置,32位系統(tǒng)下,JDK5.0以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K】,線程棧里面的數(shù)據(jù)屬于該線程私有,但是所有的線程都共享一個堆空間,堆中存放的是對象數(shù)據(jù),什么是對象數(shù)據(jù),排除法,排除基本類型以及引用類型以外的數(shù)據(jù)都將放在堆空間中。其中方法區(qū)和堆是所有線程共享的數(shù)據(jù)區(qū)。 1.程序計數(shù)器在CPU的寄存器中有一個PC寄存器,存放下一條指令地址,這里,虛擬機不使用CPU的程序計數(shù)器,自己在內(nèi)存中設立一片區(qū)域來模擬CPU的程序計數(shù)器。只有一個程序計數(shù)器是不夠的,當多個線程切換執(zhí)行時,那就單個程序計數(shù)器就沒辦法了,虛擬機規(guī)范中指出,每一條線程都有一個獨立的程序計數(shù)器。注意,Java虛擬機中的程序計數(shù)器指向正在執(zhí)行的字節(jié)碼地址,而不是下一條。 2. Java虛擬機棧Java虛擬機棧也是線程私有的,虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法執(zhí)行的時候都會創(chuàng)建一個棧幀,用于存放局部變量表,操作數(shù)棧,動態(tài)鏈接,方法出口等信息。每一個方法從調(diào)用直到執(zhí)行完成的過程都對應著一個棧幀在虛擬機中的入棧到出棧的過程。我們平時把內(nèi)存分為堆內(nèi)存和棧內(nèi)存,其中的棧內(nèi)存就指的是虛擬機棧的局部變量表部分。局部變量表存放了編譯期可以知道的基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double),對象引用(可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能指向一個代表對象的句柄或者其他與此對象相關的位置),和返回后所指向的字節(jié)碼的地址。其中64 位長度的long 和double 類型的數(shù)據(jù)會占用2個局部變量空間(Slot),其余的數(shù)據(jù)類型只占用1個。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。當遞歸層次太深時,會引發(fā)java.lang.StackOverflowError,這是虛擬機棧拋出的異常。 3. 本地方法棧在HotSpot虛擬機將本地方法棧和虛擬機棧合二為一,它們的區(qū)別在于,虛擬機棧為執(zhí)行Java方法服務,而本地方法棧則為虛擬機使用到的Native方法服務。 4. Java堆Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。這個區(qū)域是用來存放對象實例的,幾乎所有對象實例都會在這里分配內(nèi)存。堆是Java垃圾收集器管理的主要區(qū)域(GC堆),垃圾收集器實現(xiàn)了對象的自動銷毀。Java堆可以細分為:新生代和老年代;再細致一點的有Eden空間,F(xiàn)rom Survivor空間,To Survivor空間等。Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,就像我們的磁盤空間一樣??梢酝ㄟ^-Xmx和-Xms控制 5. 方法區(qū)方法區(qū)也叫永久代。在過去(自定義類加載器還不是很常見的時候),類大多是”static”的,很少被卸載或收集,因此被稱為“永久的(Permanent)”。雖然Java 虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java 堆區(qū)分開來。同時,由于類class是JVM實現(xiàn)的一部分,并不是由應用創(chuàng)建的,所以又被認為是“非堆(non-heap)”內(nèi)存。HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區(qū),或者說使用永久代來實現(xiàn)方法區(qū)而已。對于其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。 永久代也是各個線程共享的區(qū)域,它用于存儲已經(jīng)被虛擬機加載過的類信息,常量,靜態(tài)變量(JDK7中被移到Java堆),即時編譯期編譯后的代碼(類方法)等數(shù)據(jù)。這里要講一下運行時常量池,它是方法區(qū)的一部分,用于存放編譯期生成的各種字面量和符號引用(其實就是八大基本類型的包裝類型和String類型數(shù)據(jù)(JDK7中被移到Java堆))(官方文檔說明: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application)。 在JDK1.7中的HotASpot中,已經(jīng)把原本放在方法區(qū)的字符串常量池移出。
從JDK7開始永久代的移除工作,貯存在永久代的一部分數(shù)據(jù)已經(jīng)轉(zhuǎn)移到了Java Heap或者是Native Heap。但永久代仍然存在于JDK7,并沒有完全的移除:符號引用(Symbols)轉(zhuǎn)移到了native heap;字面量(interned strings)轉(zhuǎn)移到了java heap;類的靜態(tài)變量(class statics)轉(zhuǎn)移到了java heap。隨著JDK8的到來,JVM不再有PermGen。但類的元數(shù)據(jù)信息(metadata)還在,只不過不再是存儲在連續(xù)的堆空間上,而是移動到叫做“Metaspace”的本地內(nèi)存(Native memory)中。 在JVM中共享數(shù)據(jù)空間劃分如下圖所示 上圖中,刻畫了Java程序運行時的堆空間,可以簡述成如下2條 1.JVM中共享數(shù)據(jù)空間可以分成三個大區(qū),新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分為新生代和老年代 2.新生代可以劃分為三個區(qū),Eden區(qū)(存放新生對象),兩個幸存區(qū)(From Survivor和To Survivor)(存放每次垃圾回收后存活的對象) 3.永久代管理class文件、靜態(tài)對象、屬性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information ) 4.JVM垃圾回收機制采用“分代收集”:新生代采用復制算法,老年代采用標記清理算法。 作為操作系統(tǒng)進程,Java 運行時面臨著與其他進程完全相同的內(nèi)存限制:操作系統(tǒng)架構提供的可尋址地址空間和用戶空間。 操 作系統(tǒng)架構提供的可尋址地址空間,由處理器的位數(shù)決定,32 位提供了 2^32 的可尋址范圍,也就是 4,294,967,296 位,或者說 4GB。而 64 位處理器的可尋址范圍明顯增大:2^64,也就是 18,446,744,073,709,551,616,或者說 16 exabyte(百億億字節(jié))。 地址空間被劃分為用戶空間和內(nèi)核空間。內(nèi)核是主要的操作系統(tǒng)程序和C運行時,包含用于連接計算機硬件、調(diào)度程序以及提供聯(lián)網(wǎng)和虛擬內(nèi)存等服務的邏輯和基于C的進程(JVM)。除去內(nèi)核空間就是用戶空間,用戶空間才是 Java 進程實際運行時使用的內(nèi)存。 默認情況下,32 位 Windows 擁有 2GB 用戶空間和 2GB 內(nèi)核空間。在一些 Windows 版本上,通過向啟動配置添加 /3GB 開關并使用 /LARGEADDRESSAWARE 開關重新鏈接應用程序,可以將這種平衡調(diào)整為 3GB 用戶空間和 1GB 內(nèi)核空間。在 32 位 Linux 上,默認設置為 3GB 用戶空間和 1GB 內(nèi)核空間。一些 Linux 分發(fā)版提供了一個hugemem內(nèi)核,支持 4GB 用戶空間。為了實現(xiàn)這種配置,將進行系統(tǒng)調(diào)用時使用的地址空間分配給內(nèi)核。通過這種方式增加用戶空間會減慢系統(tǒng)調(diào)用,因為每次進行系統(tǒng)調(diào)用時,操作系統(tǒng)必須在地址空間之間復制數(shù)據(jù)并重置進程地址-空間映射。 下圖為一個32 位 Java 進程的內(nèi)存布局: 可尋址的地址空間總共有 4GB,OS 和 C 運行時大約占用了其中的 1GB,Java 堆占用了將近 2GB,本機堆占用了其他部分。請注意,JVM 本身也要占用內(nèi)存,就像 OS 內(nèi)核和 C 運行時一樣。 注意: 1. 上文提到的可尋址空間即指最大地址空間。 2. 對于2GB的用戶空間,理論上Java堆內(nèi)存最大為1.75G,但一旦Java線程的堆達到1.75G,那么就會出現(xiàn)本地堆的Out-Of-Memory錯誤,所以實際上Java堆的最大可使用內(nèi)存為1.5G。 在JVM運行時,可以通過配置以下參數(shù)改變整個JVM堆的配置比例 1.Java heap的大?。ㄐ律?老年代) -Xms堆的最小值 -Xmx堆空間的最大值 2.新生代堆空間大小調(diào)整 -XX:NewSize新生代的最小值 -XX:MaxNewSize新生代的最大值 -XX:NewRatio設置新生代與老年代在堆空間的大小 -XX:SurvivorRatio新生代中Eden所占區(qū)域的大小 3.永久代大小調(diào)整 -XX:MaxPermSize 4.其他 -XX:MaxTenuringThreshold,設置將新生代對象轉(zhuǎn)到老年代時需要經(jīng)過多少次垃圾回收,但是仍然沒有被回收 在上面的配置中,老年代所占空間的大小是由-XX:SurvivorRatio這個參數(shù)進行配置的,看完了上面的JVM堆空間分配圖,可能會奇怪,為啥新生代空間要劃分為三個區(qū)Eden及兩個Survivor區(qū)?有何用意?為什么要這么分?要理解這個問題,就得理解一下JVM的垃圾收集機制(復制算法也叫copy算法),步驟如下: 復制(Copying)算法 將內(nèi)存平均分成A、B兩塊,算法過程: 1. 新生對象被分配到A塊中未使用的內(nèi)存當中。當A塊的內(nèi)存用完了, 把A塊的存活對象對象復制到B塊。 優(yōu)點:簡單高效。缺點:內(nèi)存代價高,有效內(nèi)存為占用內(nèi)存的一半。 圖解說明如下所示:(圖中后觀是一個循環(huán)過程) 對復制算法進一步優(yōu)化:使用Eden/S0/S1三個分區(qū) 平均分成A/B塊太浪費內(nèi)存,采用Eden/S0/S1三個區(qū)更合理,空間比例為Eden:S0:S1==8:1:1,有效內(nèi)存(即可分配新生對象的內(nèi)存)是總內(nèi)存的9/10。 算法過程: 1. Eden S0可分配新生對象; 默認Eden:S0:S1=8:1:1,因此,新生代中可以使用的內(nèi)存空間大小占用新生代的9/10,那么有人就會問,為什么不直接分成兩個區(qū),一個區(qū)占9/10,另一個區(qū)占1/10,這樣做的原因大概有以下幾種 1.S0與S1的區(qū)間明顯較小,有效新生代空間為Eden S0/S1,因此有效空間就大,增加了內(nèi)存使用率 |
|