深入理解Java虚拟机——第二章——Java内存区域与内存溢出异常
?
運行時數據區域
Java虛擬機運行時數據區域?
程序計數器
程序計數器可以看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條所需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
為了線程切換后能恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器,各線程之間的程序計數器互不影響。這類內存區域成為“線程私有”。
如果線程執行的是Java方法,則記錄的是正在執行的虛擬機字節碼指令的地址;如果執行的是本地方法,則計數器值為空(Undefined)。
此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
Java虛擬機棧
線程私有。生命周期與線程相同。
虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。一個方法從調用到執行完,對應著一個幀棧在虛擬機棧從入棧到出棧的過程。
局部變量表存放編譯期可知的各種基本數據類型、對象引用(reference類型)和returnAddress類型(指向了一條字節碼指令的地址)。其中long和double占用2個局部變量空間,其它數據類型占用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時其需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
該區域規定了兩種異常:
- StackOverflowError:線程請求的棧深度大于虛擬機所允許的深度
- OutOfMemoryError:如果虛擬機棧可以動態擴展,擴展時無法申請到足夠的內存
本地方法棧
本地方法棧與虛擬機棧類似,虛擬機棧為Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。
虛擬機規范中對本地方法棧中方法使用的語言、使用方式與數據結構并沒有強制規定,因此具體的虛擬機可以自由實現它(Sun Hotsopt將其和虛擬機棧合二為一)。
同樣也會拋出StackOverflowError和OutOfMemoryError異常。
Java堆
Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。
幾乎所有的對象實例以及數組都要在Java堆里分配內存。
Java堆是垃圾收集器管理的主要區域,因此也被稱為“GC堆”。
Java堆可以處于物理上不連續的內存空間中,只要邏輯上是連續的。
如果堆中沒有內存完成實例分配,且堆無法擴展,會拋出OutOfMemoryError異常。
堆=新生代+老年代,不包括永久代(方法區)。
方法區
方法去與Java堆一樣是各個線程共享的內存區域。
存放已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。JDK7的HotSpot把存放在方法區的字符串常量池移出。(即方法區存類信息?)
和Java堆一樣不需要連續的內存、大小可擴展,但方法區還可以選擇不實現垃圾收集。同樣無法滿足內存分配需求會拋出OutOfMemoryError異常。
注意:Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有個別名Non-Heap,目的應該是跟Java堆區分開來。
運行時常量池
運行時常量池是方法區的一部分。
具備動態性,并非預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,例如String的intern()方法。
當常量池無法再申請到內存時OutOfMemoryError異常。
直接內存
直接內存不是虛擬機運行時內存的一部分,也不是Java虛擬機規范中定義的內存區域。但這部分內存也被頻繁使用,也可能導致OutOfMemoryError異常。
例如JDK1.4中加入的NIO(New Input/Output)類,引入基于通道和緩沖區的I/O方式,可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能避免在Java堆和Native堆中來回賦值數據。
Java堆細節
對象的創建
虛擬機遇到new指令:
- 首先檢查指令的參數是否能在常量池中定位到一個類的符號的引用,并檢查這個符號引用代表的類是否已被加載、解析和初始化過。
- 類加載檢查通過后,虛擬機為新生對象分配內存。對象所需的內存大小在類加載完成后便可完全確定。為對象分配空間等同于把一塊確定大小的內存從Java堆里劃分出來,有兩種分配方式:
- 指針碰撞:要求Java堆中內存是規整的,用一個指針作為分界點指示器來區分用過的內存和空閑的內存。分配內存就把指針向空閑空間那邊移動與對象大小相等的距離。
- 空閑列表:Java堆中內存不是是規整的,虛擬機必須維護一個列表來記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄。
選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
?對象創建是非常頻繁的,即使是僅僅修改一個指針所指向的位置,在并發情況下也不是線程安全的:可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用原來的指針來分配內存。對分配內存空間的動作有兩種解決方案:
- 同步處理:實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性
- 按照線程劃分在不同的空間中進行:即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。哪個線程需要分配內存就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時才需要同步鎖定。
內存分配完后,虛擬機會將分配到的內存空間初始化為零值(不包括對象頭),如果使用TLAB,這個工作可以提前至TLAB分配時進行。這一步操作保證對象的實例字段在Java代碼中可以不賦初始值就可以被使用,初始值為相應數據類型對應的零值。
接下去虛擬機對對象進行必要的設置,例如對象是哪個類的實例、對象的哈希碼、對象的GC分代年齡等,這些信息都存在對象頭中(Object Header)。
上面工作完成后,從虛擬機視角看,一個新的對象已經產生了,但從Java程序的視角看,對象創建才剛剛開始——<init>方法還沒有執行,所有的字段還未零。所以一般來說執行new指令后會接著執行<init>方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。
對象的內存布局
Hotspot虛擬機中,對象在內存中存儲的布局可分為3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
對象頭包括兩部分信息:
- 第一部分用于存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標志....。數據長度在32位和64位虛擬機的長度分別為32bit和64bit,官方稱“Mark Word”。但是存儲的數據多,已經超出了32位、64位Bitmap結構所能記錄的限度。這是與對象自定義的數據無關的額外成本,考慮到虛擬機的空間效率,Mark Word被設計成非固定的數據結構,以便在績效的空間內存儲盡量多的信息,它會根據對象的狀態復用自己的存儲空間。
- 另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。但并不是所有虛擬機實現都需要保存類型指針,即查找對象的元數據信息并不一定要經過對象本身。另外,如果對象是一個Java數組,對象頭還必須有一塊用于記錄數組長度的數據,因為虛擬機可以通過普通Java對象的數據信息確定Java對象的大小,但是從數組的元數據中無法確定數組的大小。
實例數據存儲程序代碼中所定義的各種類型的字段內容。無論是父類繼承下來的,還是在子類中定義的,都需要記錄。存儲順序手虛擬機分配策略參數和字段在Java源碼中定義順序的影響。
對齊填充并不是必然存在,僅起占位符的作用。由于Hotspot自動內存管理系統要求對象起始地址必須是8字節的整數倍,即對象大小必須是8字節的整數倍。對象頭正好是8字節的倍數,因此當對象實例數據部分沒有對齊時,就需要對齊填充來補全。
對象的訪問定位
Java程序通過棧上的reference數據來操作堆里的具體對象。而reference類型只規定了一個指向對象的引用,并沒有定義這個引用應該通過何種方式去定位、訪問堆中對象的具體位置(因為引用只是與對象相關的位置,有可能是對象的起始地址,也有可能是代表對象的句柄),所以對象訪問方式是取決于虛擬機實現。目前主流的訪問方式有使用句柄和直接指針兩種:
- 使用句柄訪問:Java堆會劃分出一塊內存作為句柄池,reference存儲的就是對象的句柄地址,而句柄中包含對象實例數據與類型數據各自的具體信息。
- 使用直接指針訪問:Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息
二者訪問方式各有優勢:句柄訪問的好處是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要修改;使用直接指針訪問是速度更快,節省了一次指針定位的時間開銷,對象的訪問在Java中是非常頻繁的。HotSpot使用的是第二種。
實戰:OutOfMemoryError異常
?
內存泄露即垃圾收集器無法自動回收這些內存。
單線程下,無論是幀棧太大還是虛擬機棧容量小,當內存無法分配時拋出的都是StackOverflowError異常。多線程就可以拋出OutOfMemoryError異常。(因為單線程的話除了方法區、堆容量外,剩下內存全給棧了,只有一個棧當然只會棧溢出。而多線程的話需要瓜分剩下的內存,如果剩下內存不夠瓜分就會內存溢出。)
可通過減少最大堆和減少棧容量來換取更多的線程。即有空閑的內存來換取線程所獨有的內存,例如棧內存。
運行時常量池溢出
String.intern()測試:
public static void main(String[] args) {String str1 = new StringBuilder("計算機").append("軟件").toString();System.out.println(str1.intern() == str1);String str2 = new StringBuilder("ja").append("va").toString();System.out.println(str2.intern() == str2); }
?
這段代碼在JDK6中執行結果是兩個false:JDK6中,intern會把首次遇到的字符串實例復制到永久代中(放到運行時常量池。方法區和永久代的關系很像Java中接口和類的關系,類實現了接口,而永久代就是HotSpot虛擬機對虛擬機規范中方法區的一種實現方式。),返回的也是永久代中這個字符串的引用。而StringBuilder創建的字符串實例在Java堆上,所以必然不是同一個引用。
在JDK7的執行結果是一個true和一個false:JDK7的intern()實現不會復制實例,只是在常量池記錄首次出現的實例的引用。str2返回false是因為“java”字符串在執行StringBuilder.toString()之前已經出現過,字符串常量池中已經有它的引用了,不符合首次出現的原則。(這里的意思是之前已經創建過java的字符串對象?然后intern存儲相同字符串只會存儲第一次所創建的實例引用?)
?
轉載于:https://www.cnblogs.com/yjou/p/11185469.html
總結
以上是生活随笔為你收集整理的深入理解Java虚拟机——第二章——Java内存区域与内存溢出异常的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: leetcode-93-复原ip地址
- 下一篇: AttributeError: 'dic