Sun官方定義的Java技術體系包含:Java程序設計語言,各類硬件平臺上的Java虛擬機,Class文件格式,Java API類庫和來自商業機構和開源社區的第三方Java類庫。java
Java程序設計語言,Java虛擬機,Java API類庫這三部分統稱爲JDK,JDK是用於支持Java程序開發的最小環境。Java API類庫中的Java SE API子集和Java虛擬機這兩部分稱爲JRE,JRE是支持JAVA程序運行的標準環境。算法
Java技術體系能夠分爲4個平臺,分別爲:小程序
Java Card:支持一些Java小程序運行在小內存設備上的平臺數組
Java ME(micro edtion):支持Java程序運行在移動終端上的平臺,對Java API有所精簡,並加入了針對移動終端的支持,之前稱爲J2ME安全
Java SE(standard edition):支持面向桌面級應用的Java平臺,提供了完整的Java核心API,之前稱爲J2SE數據結構
Java EE(enterprise edition):支持使用多層架構的企業應用的Java平臺。之前稱爲J2EE多線程
Java虛擬機會在執行Java程序的過程當中會把它所管理的內存劃分爲以下不一樣的數據區域:方法區(method area),虛擬機棧(vm stack),本地方法棧(native method stack),堆(heap)和程序計數器(program counter register)。架構
程序計數器併發
程序計數器是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。在JVM的概念模型裏,字節碼解釋器工做時就是經過改變計數器的值來選取下一條須要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能都須要依賴這個計數器來完成。因爲JVM的多線程是經過線程輪流切換並分配處理器質性時間的方式來實現的,在任何一個肯定的時間,一個處理器只會執行一條線程中的指令。所以每條線程都須要一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。若線程正在向執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若正在執行的是native方法,這個計數器值則爲空(Undefined)。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。編輯器
Java虛擬機棧
JVM Stack是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型,每一個方法在執行的同時會建立一個棧幀(Stack Frame)用於存儲局部變量表,操做數棧,動態連接,方法出口等。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。虛擬機棧中的局部變量表存放了編譯器可知的各類基本數據類型,對象引用和returnAddress類型(指向了一條本身碼指令的地址)。其中64位長度的long和double類型的數據會佔用2個局部變量空間(slot),其他數據類型只佔用一個。局部變量表所需的內存空間在編譯期間完成分配。當進入一個方法時,這個方法在幀中分配多大的局部變量空間時完成肯定的,在方法運行期間時不會改變局部變量表的大小。JVM規範中對JVM Stack定義了兩種異常狀況:若線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常。若虛擬機能夠動態擴展而且擴展時沒法申請到足夠的內存時,會拋出OutOfMemoryError異常。
本地方法棧
本地方法棧位JVM使用到的Native方法服務。JVM規範中對本地方法棧中使用過的語言,方式與數據結構並無強制規定。本地方法棧會拋出StackOverflowError和OutOfMemoryError異常。
Java堆
Java Heap是Java JVM所管理的內存中最大的一塊。Java Heap是被全部線程共享的一塊內存,在虛擬機啓動時建立。此內存區域的惟一目的是存放對象實例。Java Heap是垃圾收集器管理的主要區域。也稱爲GC堆(garbage collected heap)。垃圾回收器基本採用分代收集算法,所以Java Heap中還能夠細分爲新生代和老生代。從內存分配的角度來看,線程共享的Java堆中能夠劃分出多個線程私有的分配緩衝區(thread local allocation buffer, TLAB)。JVM規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是聯繫的便可。若在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,就會拋出OutOfMemoryError異常。
方法區
Method Area是各個線程共享的內存區域,用於存儲已被JVM加載的類信息,常量,靜態變量,即時編輯器編譯後的代碼等數據。JVM規範把方法去描述爲堆的一個邏輯部分,也稱Non-Heap。方法區不須要連續的內存和能夠選擇固定大小或擴展,還能夠選擇不實現垃圾回收。方法區的垃圾回收目標主要是針對常量池的回收和對類型的卸載。當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
運行時常量池(runtime constant pool)是方法區的一部分。Class文件中除了有類的版本,字段,方法,接口等描述信息外,還有一項是常量池,用於存放編譯期生成的各類字面量和符號引用。運行時常量池具有動態性,Java語言並不要求常量必定只有編譯期才能產生,即並不是預置Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也能夠將新的常量放入池中(調用String的intern()方法)。
直接內存(Direct Memory):NIO引入了一種基於通道(Channel)與緩衝區(buffer)的I/O方式,可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做。本機直接內存的分配不會受到Java堆大小的限制,可是會受到本機總內存大小以及處理器尋址空間的限制,當各個內存區域總和大於物理內存限制從而致使動態擴展時會出現OutOfMemoryError異常。
HotSpot虛擬機對象
對象的建立
JVM碰見一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並檢查這個符號引用表明的類是否已經被加載,解析和初始化過。若沒有,則必須執行相應的類加載過程。
在類加載檢查事後,JVM將爲新生對象分配內存。對象所需的內存的大小在類加載完成後即可以徹底肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從Java堆中劃分出來。假設Java堆中內存是絕對規整的,全部用過的內存都放在一邊,空閒的內存放在另外一邊,中間放着一個指針做爲分界點的指示器,那麼分配內存就僅僅是把那個指針向空閒空間那邊挪到一段與對象大小相等的距離,這種分配方式稱爲指針碰撞(bump the pointer)。若Java堆中的內存不是規整的,已使用的內存和空閒的內存想互相交錯,虛擬機就必須維護一個列表來記錄哪些內存塊是可用的,在分配時從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱爲空閒列表(free list)。選擇哪一種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。使用Serial,ParNew等帶Compact過程的收集器時,系統採用的分配算法是指針碰撞,而使用CMS這種基於Mark-Sweep算法的收集器時,一般採用空閒列表。
在併發的狀況下,建立對象多是線程不安全的。有兩種解決辦法:一種是對內存分配空間的動做進行了同步處理(JVM採用CAS配上失敗重試的方式保證更新操做的原子性),另外一種是把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(thread local allocation buffer, tlab),哪一個線程要分配內存就在哪一個線程的tlab上分配,只有tlab用完病分配新的tlab時,才須要同步,虛擬機是否使用tlab,能夠經過-XX:+/-UseTLAB參數來設定。
內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲0。
JVM要對對象進行必要的設置,如這個對象是哪一個類的實例,對象的哈希碼等,這些信息存放在對象的對象頭中。
對象的內存佈局
對象在內存中存儲的佈局能夠分爲三塊區域:對象頭(header),實例數據(instance data)和對其填充(padding)。
HotSpot JVM對象頭包含兩部分信息:一部分用於存儲對象自身的運行時數據,如哈希碼,GC分代年齡,鎖狀態標誌,線程持有的鎖,偏向線程ID,偏向時間戳等,這部分數據的長度在32位和64位的JVM中分別對應32bit和64bit。官方稱之爲「Mark Word」。對象頭信息是與對象自身定義的數據結構無關的額外存儲成本,所以Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的存儲空間。在對象處於未被鎖定的狀態下,Mark Word的32bit空間中的25bit用於存儲對象哈希碼,4bit用於存儲對象分代年齡,2bit用於存儲鎖標誌位,1bit固定爲0。
另外一部分是類型指針,即對象指向它的類元數據指針,虛擬機經過這個指針來肯定對象是哪一個類的實例。並非全部的虛擬機實現都必須在對象數據上保留類型指針,即查找對象的元數據信息並不必定要通過對象自己。若對象是一個java數組,那在表頭中還必須有一塊記錄數組長度的數據。實例數據部分是真正存儲的有效信息,也是在程序代碼中所定義的字段內容。這部分存儲順醋會受到JVM分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義的順序影響。HotSpot虛擬機默認的分配策略爲longs/doubles,ints,shorts/chars,bytes/booleans, oops(Ordinary Object Pointers),從分配策略上來看,相同寬度的字段老是被分配到一塊兒。在知足這個前提下,在父類中定義的變量會出如今子類以前。
對其填充並非必然存在的,它僅僅起着佔位符的做用。HotSpot VM的自動內存管理系統要求對象其實地址必須是8字節的整數倍,當對象實例數據部分沒有對齊,則須要經過對其填充來補全。
存儲內容 | 標誌位 | 狀態 |
對象哈希碼,對象分代年齡 | 01 | 未鎖定 |
只向鎖記錄的指針 | 00 | 輕量級鎖定 |
只向中量級鎖的指針 | 10 | 膨脹 |
空,不須要記錄信息 | 11 | GC標記 |
偏向線程ID,偏向時間戳,對象分代年齡 | 01 | 可偏向 |
對象的訪問定位
Java程序須要經過棧上的reference數據來操做堆上的具體對象。reference類型在Java虛擬機規範中只規定了一個指向對象的引用,並無定義這個引用應該經過何種方式去定位,訪問堆中對象的具體位置,因此對象訪問方式也是取決於虛擬機實現而定的。目前主流的訪問方式有使用句柄和直接指針兩種。
使用句柄訪問,Java堆中將會劃分出一塊內存做爲句柄池,reference中存儲的幾句是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。
使用直接指針訪問,Java堆對象的佈局中必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接地址就是對象地址。
使用句柄訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而reference自己不須要修改。使用直接指針訪問方式的最大好處就是速度更快,節省了一次指針定位的時間開銷。
Java堆溢出
只要不斷建立對象而且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼對象數量達到最大堆的容量限制後就會產生內存溢出異常。當出現Java堆內存溢出時,異常堆棧信息"java.lang.OutOfMemoryError"會跟着進一步提示"Java heap space"。解決這個區域的異常,通常的手段是先經過內存映像分析工具(Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也要了解究竟是出現了內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)。若內存泄漏,能夠經過工具查看泄漏對象到GC Roots的引用鏈,因而就能夠找到泄漏對象是經過怎樣的路徑與GC Roots相關聯並致使垃圾收集器沒法自動回收他們的。若不存在內存泄漏,則說明內存中的對象確實是必須存活的,此時應該檢查虛擬機的堆參數(-Xmx與-Xms),與烏力吉內存對比看是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長,持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。
虛擬機棧和本地方法棧溢出
HotSpot虛擬機並不區分虛擬機棧和本地方法棧,雖然-Xoss參數能夠設置本地方法棧的大小,但其實是無效的,棧容量只由-Xss參數設定。關於虛擬機棧和本地方法棧,在Java虛擬機規範中描述了兩種異常:若線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError異常;若虛擬機在擴張棧時沒法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
在單個線程下,不管是由棧幀太大仍是虛擬機棧容量大小,當內存沒法分配的時候,虛擬機拋出的都是StackOverflowError異常。
若不限制單線程,經過不斷地創建線程的方式能夠產生內存溢出異常。爲每一個線程的棧分配的內存越大,反而越容易產生內存溢出異常。所以操做系統分配給每進程的內存是有限制,虛擬機提供了參數來控制Java堆和方法區的這兩部份內存的最大值。剩餘的內存減去Xmx(最大堆容量),再減去MaxPermSize(最大方法區容量)。若虛擬機進程自己耗費的內存不計算在內,剩下的內存就由虛擬機棧和本地方法棧瓜分了。每一個線程分配到的棧容量越大,可創建的線程數量天然越少,創建線程越容易把剩下的內存耗盡。若創建過多線程致使內存溢出,再不能減小線程或更換64位虛擬機的狀況下,就只能經過減小最大堆和減小棧容量來換取更多的線程。
方法區和運行時常量池溢出
String.intern()是一個Native方法,它的做用是:若字符串常量池中已經包含一個等一此String對象的字符串,則返回表明池中的這個字符串的String對象;不然將此String對象包含的字符串添加到常量池中,而且返回此String對象的引用。能夠經過-XX:PermSIze和-XX:MaxPermSize限制方法區大小,從而間接限制其中常量池的容量。運行時常量池溢出,在OutOfMemoryError後面跟隨的提示信息是"PermGen space"。
本機直接內存溢出
DirectMemory容量可經過-XX:MaxDirectMemorySIze指定,若不指定,則默認與Java堆最大值同樣。由DirectMemory致使的內存溢出,一個明顯特徵是在Heap Dump文件中不會看見明顯異常,若發現OOM以後Dump文件很小,而程序中又直接或間接使用了NIO,能夠考慮下是本機直接內存溢出。