天天看點

JVM:Java記憶體區域與記憶體溢出

一.運作時資料區域

6個。單個線程獨有:線程程式計數器,虛拟機棧,本地方法棧。所有線程共有:JAVA堆,方法區,運作時常量池(本屬方法區,java虛拟機劃分出來)

1.程式計數器:一塊較小的記憶體空間,它是目前線程所執行的位元組碼的行号訓示器,位元組碼解釋器工作時通過改變該計數器的值來選擇下一條需要執行的位元組碼指令,分支、跳轉、循環等基礎功能都要依賴它來實作。每條線程都有一個獨立的的程式計數器,各線程間的計數器互不影響,是以該區域是線程私有的。

當線程在執行一個Java方法時,該計數器記錄的是正在執行的虛拟機位元組碼指令的位址,當線程在執行的是Native方法(調用本地作業系統方法)時,該計數器的值為空。另外,該記憶體區域是唯一一個在Java虛拟機規範中麼有規定任何OOM(記憶體溢出:OutOfMemoryError)情況的區域。

2.java虛拟機棧:

線程私有,生命周期與線程相同。虛拟機棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀,對于執行引擎來講,活動線程中,隻有棧頂的棧幀是有效的,稱為目前棧幀,這個棧幀所關聯的方法稱為目前方法,執行引擎所運作的所有位元組碼指令都隻針對目前棧幀進行操作。棧幀用于存儲局部變量表、操作數棧、動态連結、方法傳回位址和一些額外的附加資訊。在編譯程式代碼時,棧幀中需要多大的局部變量表、多深的操作數棧都已經完全确定了,并且寫入了方法表的Code屬性之中。是以,一個棧幀需要配置設定多少記憶體,不會受到程式運作期變量資料的影響,而僅僅取決于具體的虛拟機實作。

兩種異常情況:

1、如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常。

2、如果虛拟機在動态擴充棧時無法申請到足夠的記憶體空間,則抛出OutOfMemoryError異常。
           

3. 本地方法棧(Native Method Stacks)

該區域與虛拟機棧所發揮的作用非常相似,隻是虛拟機棧為虛拟機執行Java方法服務,而本地方法棧則為使用到的本地作業系統(Native)方法服務。

4.java堆:

Java Heap是Java虛拟機所管理的記憶體中最大的一塊,它是所有線程共享的一塊記憶體區域。幾乎所有的對象*執行個體和數組*都在這類配置設定記憶體。Java Heap是垃圾收集器管理的主要區域,是以很多時候也被稱為“GC堆”。

根據Java虛拟機規範的規定,Java堆可以處在實體上不連續的記憶體空間中,隻要邏輯上是連續的即可。如果在堆中沒有記憶體可配置設定時,并且堆也無法擴充時,将會抛出OutOfMemoryError異常。

5.方法區 :方法區也是各個線程共享的記憶體區域,它用于存儲已經被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。方法區域又被稱為“永久代”,但這僅僅對于Sun HotSpot來講,JRockit和IBM J9虛拟機中并不存在永久代的概念。Java虛拟機規範把方法區描述為Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的記憶體,可以選擇固定大小或可擴充,另外,虛拟機規範允許該區域可以選擇不實作垃圾回收。相對而言,垃圾收集行為在這個區域比較少出現。該區域的記憶體回收目标主要針是對廢棄常量的和無用類的回收。

6。.運作時常量池

運作時常量池是方法區的一部分,Class檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有一項資訊是常量池(Class檔案常量池),用于存放編譯器生成的各種字面量和符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中。運作時常量池相對于Class檔案常量池的另一個重要特征是具備動态性,Java語言并不要求常量一定隻能在編譯期産生,也就是并非預置入Class檔案中的常量池的内容才能進入方法區的運作時常量池,運作期間也可能将新的常量放入池中,這種特性被開發人員利用比較多的是String類的intern()方法。

根據Java虛拟機規範的規定,當方法區無法滿足記憶體配置設定需求時,将抛出OutOfMemoryError異常。

直接記憶體:直接記憶體并不是虛拟機運作時資料區的一部分,也不是Java虛拟機規範中定義的記憶體區域,它直接從作業系統中配置設定,是以不受Java堆大小的限制,但是會受到本機總記憶體的大小及處理器尋址空間的限制,是以它也可能導緻OutOfMemoryError異常出現。在JDK1.4中新引入了NIO機制,它是一種基于通道與緩沖區的新I/O方式,可以直接從作業系統中配置設定直接記憶體,即在堆外配置設定記憶體,這樣能在一些場景中提高性能,因為避免了在Java堆和Native堆中來回複制資料。

二.記憶體溢出

各個記憶體區域溢出測試

JVM:Java記憶體區域與記憶體溢出

多線程情況下,每個線程的棧配置設定的記憶體越大,越容易産生記憶體溢出異常。作業系統為每個程序配置設定的記憶體是有限制的,虛拟機提供了參數來控制Java堆和方法區這兩部分記憶體的最大值,忽略掉程式計數器消耗的記憶體(很小),以及程序本身消耗的記憶體,剩下的記憶體便給了虛拟機棧和本地方法棧,每個線程配置設定到的棧容量越大,可以建立的線程數量自然就越少。是以,如果是建立過多的線程導緻的記憶體溢出,在不能減少線程數的情況下,就隻能通過減少最大堆和每個線程的棧容量來換取更多的線程。

另外,由于Java堆内也可能發生記憶體洩露(Memory Leak),這裡簡要說明一下記憶體洩露和記憶體溢出的差別:

記憶體洩露是指配置設定出去的記憶體沒有被回收回來,由于失去了對該記憶體區域的控制,因而造成了資源的浪費。Java中一般不會産生記憶體洩露,因為有垃圾回收器自動回收垃圾,但這也不絕對,當我們new了對象,并儲存了其引用,但是後面一直沒用它,而垃圾回收器又不會去回收它,這邊會造成記憶體洩露,

記憶體溢出是指程式所需要的記憶體超出了系統所能配置設定的記憶體(包括動态擴充)的上限。

對象執行個體化分析

對記憶體配置設定情況分析最常見的示例便是對象執行個體化:

Object obj = new Object();

這段代碼的執行會涉及java棧、Java堆、方法區三個最重要的記憶體區域。假設該語句出現在方法體中,及時對JVM虛拟機不了解的Java使用這,應該也知道obj會作為引用類型(reference)的資料儲存在Java棧的本地變量表中,而會在Java堆中儲存該引用的執行個體化對象,但可能并不知道,Java堆中還必須包含能查找到此對象類型資料的位址資訊(如對象類型、父類、實作的接口、方法等),這些類型資料則儲存在方法區中。

另外,由于reference類型在Java虛拟機規範裡面隻規定了一個指向對象的引用,并沒有定義這個引用應該通過哪種方式去定位,以及通路到Java堆中的對象的具體位置,是以不同虛拟機實作的對象通路方式會有所不同,主流的通路方式有兩種:使用句柄池和直接使用指針。

通過句柄池通路的方式如下:
           
JVM:Java記憶體區域與記憶體溢出

通過直接指針通路的方式如下:

JVM:Java記憶體區域與記憶體溢出
這兩種對象的通路方式各有優勢,使用句柄通路方式的最大好處就是
           

reference中存放的是穩定的句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。使用直接指針通路方式的最大好處是速度快,它節省了一次指針定位的時間開銷。目前Java預設使用的HotSpot虛拟機采用的便是是第二種方式進行對象通路的。