摘要:內存區域虛擬機在運行程序時,會將其管理的內存區域劃分成若干個不同的數據區域。運行時常量池運行時常量池是方法區的一部分。另外一部分官方稱為用于存儲自身運行時的數據,比如哈希值年齡鎖狀態標志偏向線程等。
前言
最近一直在看周志明老師的《深入理解虛擬機》,總是看了忘,忘了又看,陷入這樣無休止的循環當中。抱著紙上得來終覺淺的想法,準備陸續的寫幾篇學習筆記,梳理知識的脈絡并強化一下對知識的掌握。(本文遠遠談不上深入,但為了博瀏覽量,請原諒我這個標題黨)。
概述"Write Once,Run Anywhere"是sun公司用來展示java語言跨平臺特性的口號。這標示著java語言可以在任何機器上開發,并編譯成標準的字節碼,在任何具有jvm虛擬機上的設備運行,這也是java語言早期興起的關鍵。java另一大特性是其虛擬機的內存自動管理機制,這使得java程序員在創建任何一個對象時都不需要去寫與之配對的delete/free代碼(釋放內存),不容易出現因為粗心大意而導致的內存泄漏和內存溢出的問題。可是因為將內存管理的權利交給虛擬機,一旦出現內存泄漏和內存溢出的問題,如果我們不了解虛擬機相關的知識,排查問題將是一件極為艱難的事情。
java內存區域java虛擬機在運行java程序時,會將其管理的內存區域劃分成若干個不同的數據區域。接下來的知識如果沒有指明jdk版本號,統一以jdk1.6為標準,內存區域如下圖所示:
程序計數器
程序計數器是一塊較小的內存區域,可以把它看成是當前線程執行字節碼的行號指示器。由于java虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式實現的。在任意一個確定的時刻,一個cpu核心只會執行一個線程,因此為了cpu在切換線程后可以找到上次運行的位置,每條線程都應該有一個獨立的程序計數器。各個線程間的程序計數器應互不影響并獨立存儲。如果此時運行的是java方法,這個記錄器記錄的是正在執行虛擬機字節碼指令的地址,如果執行的是native方法,則這個計數器為空。此內存區域也是唯一一個java虛擬機規范里沒有規定任何OutOfMemoryError情況的區域。
java虛擬機棧
虛擬機棧也是線程私有的,它的生命周期和線程是相同的,它描述的就是java方法執行的內存區域。每個方法在執行的同時都會創建一個棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用到執行完成就對應著一個棧幀在虛擬機中從入棧到出棧的過程。如果線程請求的深入大于棧所允許的深度,就會拋出StackOverflowError異常,大部分虛擬機支持動態擴展,如果擴展時無法申請到足夠的內存,則會拋出OutOfMemoryError異常.
局部變量表:存放了編譯器可知的各種基本數據類型(8種基礎類型)、對象的引用(reference類型)和returnAddress類型(指向了一條字節碼指令的地址),局部變量表在編譯期是就可確定其大小。
操作數棧:也是棧的一種,虛擬機把操作數棧作為它的工作區,大多數指令都要從這里彈出數據,執行運算,然后把結果壓回操作數棧。
動態鏈接: Class 文件中存放了大量的符號引用,字節碼中的方法調用指令就是以常量池中指向方法的符號引用作為參數。這些符號引用一部分會在類加載階段或第一次使用時轉化為直接引用,這種轉化稱為靜態解析。另一部分將在每一次運行期間轉化為直接引用,這部分稱為動態連接.可以簡單的理解為為了支持在方法中使用靜態變量和常量...
方法出口:一般來說只有兩種方法出口。一種是正常執行完畢,可以講程序計數器作為返回地址返回,另外一種就是拋出異常,此時返回地址為空,需要異常處理器來確定返回地址。
本地方法棧
本地方法棧和虛擬機棧的作用是非常相似的。他們之間的區別不過就是一個為java方法服務,另外一個為native方法使用。本地方法棧的實現由java虛擬機規范所定義,各大虛擬機廠商在虛擬機規范的基礎上自由實現.
java堆
java堆是所有線程共享的內存區域,也是大多數應用中虛擬機管理內存區域最大的一塊,在虛擬機啟動時創建。其作用就是為了存放對象實例。從內存回收的角度看,現在的收集器基本都采用分代收集算法。所以java堆還可以分為新生代和老年代。其中新生代又可分為Eden空間、From Survivor空間、To Survivor空間。堆內存區域的大小是通過-Xmx和-Xms來控制。如果在堆中沒有完成內存實例分配,并且堆也無法擴展,將會拋出OutOfMemoryError異常。
方法區
方法區也是所有線程共享的內存區域,它用于存儲已經被虛擬機加載的類信息、常量、靜態變量等數據。它有一個別名叫做Non-heap(非堆),目的就是為了和堆做區分。對于經常在hotspot虛擬機上的開發者來說,更愿意將方法區成為永久代,本質上兩者并不等價。只不過jvm設計團隊選擇把gc分代收集擴展至方法區,或者說使用永久代來實現方法區。但就目前發展來看,這樣并不是一個好做法。jdk1.7中已經將原本放在永久代的字符串常量池移出,jdk1.8已經完全廢除永久代這個概念,改用metaspace(元空間)。這塊區域的回收主要針對常量池的回收和對類型的卸載,條件相當的苛刻,一般回收成績也很難讓人滿意,但對其回收是非常有必要的。Sun公司的bug列表中,曾經出現多個嚴重的bug就是因為低版本的虛擬機未對方法區進行回收。當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
運行時常量池
運行時常量池是方法區的一部分。Class文件除了有類的版本、字段、方法、接口等描述信息等,還有一項信息是常量池,常量池在經過類加載后進入方法區的運行時常量池中存放。運行時常量池的一個重要特征就是具備動態性,java語言允許在運行期加新的常量放入池中。運行時常量池是方法去的一部分,自然會受到方法區內存的限制,當無法申請到內存時將會拋出OutOfMemory異常。
直接內存
直接內存并不是jvm內存管理區域的一部分,但也被頻繁的使用,并可能導致OOM一場出現。在jdk1.4之后新加入了nio(new Input/Output)類,引入了一種基于通道(channel)和緩沖區(Buffer)的I/O方式,它使用native函數分配堆外內存。它不會說到java堆大小的限制,但是會受到本機總內存的限制。在配置虛擬機參數時,經常會忽略直接內存,從而導致動態擴展時出現OOM異常。
對象的創建
在java語言層面,對象的創建通過new關鍵字來就可以實現。在jvm層面,對象(僅限于普通對象,不包括數組和class對象)的創建又是什么樣子的呢?
當虛擬機接收到一條new指令時,會先跟據new指令的參數去常量池查詢這個類的符號引用,并檢查這個類是否已經被虛擬機加載、解析、初始化。如果沒有,則要先執行相應的類加載過程。接下來要為對象分配內存,假設堆內存是絕對規整的,只需要一個指針作為臨界點來標記內存已使用和內存未使用的區域,每次分配對象只需要移動與對象大小相等的距離即可,這種內存分配方式叫做"指針碰撞"。如果堆內存不是絕對規整的,我們無法通過簡單的指針碰撞去分配內存,這時就需要虛擬機去維護一個列表,記錄哪些內存區域是未使用的和其內存區域的大小,給對象分配內存只需要去空閑列表里找到一個塊足夠大的內存劃分給對象實例即可,這種方式叫做“空閑列表”。
在一個應用程序中,創建對象是非常頻繁的行為,僅僅是一個指針的分配在并發情況下都不是絕對安全的。很有可能正在給A對象分配內存,指針還沒來得及修改位置,又發生著使用原來的指針給B對象分配內存。jvm提供了兩種解決方案,1.jvm使用cas配上失敗重試來保證指針更新操作的原子性。2.將內存分配的動作按照線程分區域進行,也就是預先給每個線程申請一部分區域,這種方式稱為本地緩沖(Thread Local Allocate Buffer,TLAB).哪個線程要分配對象就在哪個線程的tlab上分配。只有當tlab用完并分配新的tlab才需要同步鎖定,虛擬機是否開啟tlab可以通過參數-XX:+/UseTLAB來決定。
內存分配好后,jvm需要分配的內存都初始化為零值(不包括對象頭),以便java代碼中變量不賦值,也可以訪問到其數據類型對應的零值。接下來需要對對象頭部分來做一個設置,對象頭中主要包括類的元信息,對象的哈希碼,對象的gc分代年齡以及鎖記錄等,在上面這些工作都完成時,從虛擬機的角度來說一個對象就已經創建好了。但從java語言的角度來看,還需要其執行構造方法讓其按照程序員的意愿去構造這個對象,這樣一個真正可用的對象才算完全產生出來。
對象的內存布局
對象的主要分為三部分對象頭(Object Header),實例數據(Instance Data)和對齊填充(Padding).
對象頭主要分為兩部分,一部分是類型指針,通過類型指針指向類的元數據(確定對象是哪個類的實例)。另外一部分官方稱為"Mark Word",用于存儲自身運行時的數據,比如哈希值、gc年齡、鎖狀態標志、偏向線程id等?!癕ark word”的存儲內容如下圖所示:
實例數據存儲的是真正有效的數據,也是我們業務所關心的數據。
對齊填充并不是必須存在的,只是因為hotspot要求對象的大小必須是8bit的整數倍,而"Mark Word"又一定是8的整數倍,實例數據大小不確定,所以用對齊填充來補充其空余的地方。
對象的訪問定位
創建對象是為了訪問對象。我們在需要通過java虛擬機棧的reference引用去獲取堆上的具體對象。但是并沒有規定如何通過一個引用具體的定位訪問到一個對象,所以對想得訪問方式也是由虛擬機的實現定義的。主流的實現方式有使用句柄和直接指針兩種。如下圖所示:
使用句柄池其最大的好處就是保證reference引用中句柄的穩定,reference引用存放的是句柄池的地址,句柄中保存了指向對象實例數據和對象類型數據的指針,在虛擬機gc的時候,對象會發生非常頻繁的移動,這個時候只要修改句柄指向對象數據的指針即可,不需要修改reference.
使用直接指針的好處就是塊,可以減少一次指針定位。由于訪問對象在一個程序中將是非常頻繁的操作,積少成多,所以這也是一個非??捎^的優化。
經過一長串的的理論分析,我們已經大致清楚java的內存區域,現在我們使用具體的例子來驗證。會將jvm的參數放在代碼注釋中。
java堆溢出
/** * -XX:+PrintGCDetails -Xmx20m -Xms20m */ public class HeapOOM { static class OOMObjectt { } public static void main(String[] args) { Listlist = new ArrayList (); try { while (true){ list.add(new OOMObjectt()); } } catch (Exception e) { } } }
其運行結果如下:
[GC [PSYoungGen: 5898K->480K(6656K)] 5898K->3769K(20480K), 0.0043241 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] [GC [PSYoungGen: 6315K->488K(6656K)] 9604K->8320K(20480K), 0.0064706 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [Full GC [PSYoungGen: 6632K->0K(6656K)] [ParOldGen: 10997K->13393K(13824K)] 17629K->13393K(20480K) [PSPermGen: 3164K->3163K(21504K)], 0.1786099 secs] [Times: user=0.58 sys=0.00, real=0.18 secs] [Full GC [PSYoungGen: 3031K->3001K(6656K)] [ParOldGen: 13393K->13393K(13824K)] 16425K->16394K(20480K) [PSPermGen: 3163K->3163K(21504K)], 0.1063835 secs] [Times: user=0.64 sys=0.02, real=0.11 secs] [Full GC [PSYoungGen: 3001K->3001K(6656K)] [ParOldGen: 13393K->13377K(13824K)] 16394K->16378K(20480K) [PSPermGen: 3163K->3163K(21504K)], 0.0873232 secs] [Times: user=0.28 sys=0.02, real=0.09 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2245) at java.util.Arrays.copyOf(Arrays.java:2219) at java.util.ArrayList.grow(ArrayList.java:242) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) at java.util.ArrayList.add(ArrayList.java:440) at HeapOOM.main(HeapOOM.java:17) Heap PSYoungGen total 6656K, used 3144K [0x00000000ff900000, 0x0000000100000000, 0x0000000100000000) eden space 6144K, 51% used [0x00000000ff900000,0x00000000ffc12240,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen total 13824K, used 13377K [0x00000000feb80000, 0x00000000ff900000, 0x00000000ff900000) object space 13824K, 96% used [0x00000000feb80000,0x00000000ff890578,0x00000000ff900000) PSPermGen total 21504K, used 3194K [0x00000000f9980000, 0x00000000fae80000, 0x00000000feb80000) object space 21504K, 14% used [0x00000000f9980000,0x00000000f9c9ebc8,0x00000000fae80000)
虛擬機棧和本地方法棧溢出
public class JavaVMStackSOF { private int stackLength=1; public void stackLeak(){ stackLength++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Exception e) { System.out.println("e.length:"+oom.stackLength); e.printStackTrace(); } } }
Exception in thread "main" java.lang.StackOverflowError at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:6) at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7) at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7) at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)
方法區和運行時常量池溢出
前文提到hotspot虛擬機棧中方法區是由永久代來實現的,可以用參數-XX:PermSize -XX:MaxPermSize來限制其空間,當無法申請到足夠的內存時,會出現“permgen space”異常。但在jdk1.7中已經將永久代的字符串常量池移除,將其移入到Class對象末尾(也就是gc heap)。在jdk1.8將廢除永久代,引用元空間概念,使用native memory來實現,可以通過參數:-XX:MetaspaceSize -XX:MaxMetaspaceSize來指定元空間大小。
/** * vm args:-XX:PermSize=4m -XX:MaxPermSize=4m -Xmx6m * Created by zhizhanxue on 18-3-26. */ public class MethodAreaOOM { public static void main(String[] args) { long i=0; Listlist = new ArrayList<>(); while (true){ list.add(String.valueOf(i++).intern()); } } }
jdk1.6的運行結果:
jdk1.7的運行結果:
jdk1.8的運行結果:
下面我們來驗證下元空間的例子:
/** *-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m */ public class SpringTest { static class OOM implements MethodInterceptor{ public Object getInstance(){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOM.class); enhancer.setCallback(this); enhancer.setUseCache(false); return enhancer.create(); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invoke(o,objects); } } public static void main(String[] args) throws ExecutionException, InterruptedException { List
運行結果:
本機直接內存溢出
DirectMemory容量可以通過參數-XX:MaxDirectMemorySize來指定,如果不指定則默認與java堆最大值(-Xmx指定一樣),代碼通過unsafe.allocateMemory()去申請堆外內存模擬本地內存溢出異常。
/** * -Xmx220m -XX:MaxDirectMemorySize=10m */ public class LocalOOM { public static void main(String[] args) throws IllegalAccessException { Field field = Unsafe.class.getDeclaredFields()[0]; field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); while (true){ unsafe.allocateMemory(1024*1024); } } }
運行結果:
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at LocalOOM.main(LocalOOM.java:12)
由堆外內存導致的內存溢出,一般都是gc日志很少,且堆dump文件不會看到明顯的異常,如果情況和上述類似,你的項目中又使用了NIO,可以著重檢查下是不是這方面的原因。
下節預告1.對象已死?(如何判斷對象是否存活)
2.垃圾收集的四種基礎算法
3.垃圾收集器的介紹
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71042.html
摘要:運行時數據區域的學習,是學習以及機制的基礎,也是深入理解對象創建及運行過程的前提。了解內存區域劃分,是學習概念的前提。 Java 運行時數據區域的學習,是學習 jvm 以及 GC 機制的基礎,也是深入理解 java 對象創建及運行過程的前提。廢話不多說,直接進入正題: 一張圖總結 showImg(https://segmentfault.com/img/bVOMAn?w=685&h=5...
摘要:抽時間重新讀了一遍深入理解一書。驗證確保文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。可見性可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。 抽時間重新讀了一遍《深入理解JVM》一書。以下為摘錄內容。 1 java內存區域 showImg(https://segmentfault.com/img/bVboDgk?w=617&h=365...
摘要:一內存區域虛擬機在運行時,會把內存空間分為若干個區域,根據虛擬機規范版的規定,虛擬機所管理的內存區域分為如下部分方法區堆內存虛擬機棧本地方法棧程序計數器。前言 在JVM的管控下,Java程序員不再需要管理內存的分配與釋放,這和在C和C++的世界是完全不一樣的。所以,在JVM的幫助下,Java程序員很少會關注內存泄露和內存溢出的問題。但是,一旦JVM發生這些情況的時候,如果你不清楚JVM內存的...
閱讀 914·2021-11-22 13:54
閱讀 2843·2021-09-28 09:36
閱讀 2980·2019-08-30 15:55
閱讀 1952·2019-08-30 15:44
閱讀 544·2019-08-29 12:31
閱讀 2564·2019-08-28 18:18
閱讀 1199·2019-08-26 13:58
閱讀 1383·2019-08-26 13:44