1. 程式人生 > >java記憶體管理(堆、棧、方法區)

java記憶體管理(堆、棧、方法區)

java記憶體管理

簡介   

       首先我們要了解我們為什麼要學習java虛擬機器的記憶體管理,不是java的gc垃圾回收機制都幫我們釋放了記憶體了嗎?但是在寫程式的過程中卻也往往因為不懂記憶體管理而造成了一些不容易察覺到的記憶體問題,並且在記憶體問題出現的時候,也不能很快的定位並解決。因此,瞭解並掌握Java的記憶體管理是我們必須要做的是事,也只有這樣才能寫出更好的程式,更好地優化程式的效能。

概述   

       Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干不同的資料區域,這些區域都有各自的用途以及建立和銷燬的時間。Java虛擬機器所管理的記憶體將會包括以下幾個執行時資料區域,如下圖所示:

 我認為我們最重要的是瞭解棧記憶體(Stack)和堆記憶體(Heap)和方法區(Method Area)這三部分,這樣我們對於初學者就簡單了許多,也更容易我們理解

程式計數器(瞭解)

程式計數器,可以看做是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器的概念模型裡,位元組碼直譯器工作就是通過改變程式計數器的值來選擇下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都要依賴這個計數器來完成。

Java虛擬機器棧(瞭解)

Java虛擬機器棧也是執行緒私有的 ,它的生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變量表、運算元棧、動態連結串列、方法出口資訊等。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。 區域性變量表中存放了編譯器可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用和returnAddress型別(指向了一條位元組碼指令的地址)。 如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。

本地方法棧(瞭解)

本地方法棧與虛擬機器的作用相似,不同之處在於虛擬機器棧為虛擬機器執行的Java方法服務,而本地方法棧則為虛擬機器使用到的Native方法服務。有的虛擬機器直接把本地方法棧和虛擬機器棧合二為一。 會丟擲stackOverflowError和OutOfMemoryError異常。

Java堆

堆記憶體用來存放由new建立的物件例項和陣列。(重點)   

Java堆是所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立,此記憶體區域的唯一目的就是存放物件例項 。   

Java堆是垃圾收集器管理的主要區域。由於現在收集器基本採用分代回收演算法,所以Java堆還可細分為:新生代和老年代。從記憶體分配的角度來看,執行緒共享的Java堆中可能劃分出多個執行緒私有的分配緩衝區(TLAB)。   

Java堆可以處於物理上不連續的記憶體空間,只要邏輯上連續的即可。在實現上,既可以實現固定大小的,也可以是擴充套件的。   

如果堆中沒有記憶體完成例項分配,並且堆也無法完成擴充套件時,將會丟擲OutOfMemoryError異常。

Java棧   

在棧記憶體中儲存的是堆記憶體空間的訪問地址,或者說棧中的變數指向堆記憶體中的變數(Java中的指標)(重點)。   

Java棧是Java方法執行的記憶體模型每個方法在執行的同時都會建立一個棧幀的用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫直至執行完成的過程就對應著一個棧幀在虛擬機器中入棧和出棧的過程。

堆和棧的聯絡   

當在堆中產生了一個數組或者物件時,可以在棧中定義一個特殊的變數,讓棧中的這個變數的取值等於陣列或物件在堆記憶體中的首地址,棧中的這個變數就成了陣列或物件的引用變數,以後就可以在程式中使用棧中的引用變數來訪問堆中的陣列或者物件,引用變數就相當於是為陣列或者物件起的一個名稱。引用變數是普通的變數,定義時在棧中分配,引用變數在程式執行到其作用域之外後被釋放。而陣列和物件本身在堆中分配,即使程式執行到使用new產生陣列或者物件的語句所在的程式碼塊之外,陣列和物件本身佔據的記憶體不會被釋放,陣列和物件在沒有引用變數指向它的時候,才變為垃圾,不能在被使用,但仍然佔據記憶體空間不放,在隨後的一個不確定的時間被垃圾回收器收走(釋放掉)。例如

由上圖我們知道,物件名稱p被儲存在了棧記憶體中,具體例項儲存在堆記憶體中。也就是說,在棧記憶體中儲存的是堆記憶體空間的訪問地址,或者說棧中的變數指向堆記憶體中的變數(Java中的指標)。

堆和棧的比較   

從堆和棧的功能和作用來通俗的比較,堆主要用來存放物件的,棧主要是用來執行程式的.而這種不同又主要是由於堆和棧的特點決定的:   

在程式設計中,例如C/C++中,所有的方法呼叫都是通過棧來進行的,所有的區域性變數,形式引數都是從棧中分配記憶體空間的。實際上也不是什麼分配,只是從棧頂向上用就行,就好像工廠中的傳送帶一樣,Stack Pointer會自動指引你到放東西的位置,你所要做的只是把東西放下來就行.退出函式的時候,修改棧指標就可以把棧中的內容銷燬.這樣的模式速度最快, 當然要用來執行程式了.需要注意的是,在分配的時候,比如為一個即將要呼叫的程式模組分配資料區時,應事先知道這個資料區的大小,也就說是雖然分配是在程式執行時進行的,但是分配的大小多少是確定的,不變的,而這個”大小多少”是在編譯時確定的,不是在執行時.   

堆是應用程式在執行的時候請求作業系統分配給自己記憶體,由於從作業系統管理的記憶體分配,所以在分配和銷燬時都要佔用時間,因此用堆的效率非常低.但是堆的優點在於,編譯器不必知道要從堆裡分配多少儲存空間,也不必知道儲存的資料要在堆裡停留多長的時間,因此,用堆儲存資料時會得到更大的靈活性。事實上,面向物件的多型性,堆記憶體分配是必不可少的,因為多型變數所需的儲存空間只有在執行時建立了物件之後才能確定.在C++中,要求建立一個物件時,只需用 new命令編制相關的程式碼即可。執行這些程式碼時,會在堆裡自動進行資料的儲存.當然,為達到這種靈活性,必然會付出一定的代價:在堆裡分配儲存空間時會花掉更長的時間。

方法區   

方法區是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料 (重點)。   

相對而言,垃圾收集行為在這個區域比較少出現,但並非資料進了方法區就永久的存在了,這個區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝。   

當方法區無法滿足記憶體分配需要時,將丟擲OutOfMemoryError異常。   

執行時常量池: 是方法區的一部分,它用於存放編譯期生成的各種字面量和符號引用。

Java GC機制   

Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區別之一,作為Java開發者,一般不需要專門編寫記憶體回收和垃圾清理程式碼,對記憶體洩露和溢位的問題,也不需要像C程式設計師那樣戰戰兢兢。這是因為在Java虛擬機器中,存在自動記憶體管理和垃圾清掃機制。概括地說,該機制對 JVM(Java Virtual Machine)中的記憶體進行標記,並確定哪些記憶體需要回收,根據一定的回收策略,自動的回收記憶體,永不停息(Nerver Stop)的保證JVM中的記憶體空間,放置出現記憶體洩露和溢位問題。   

gc回收的是無用物件,而物件建立後再jvm堆中所以我們要先來看jvm堆 JVM堆分為  

(1) 新域:儲存所有新成生的物件(使用“停止-複製”演算法進行清理)    

          新生代記憶體分為2部分,1部分 Eden區較大,1部分Survivor比較小,並被劃分為兩個等量的部分。   (

2) 舊域:新域中的物件,經過了一定次數的GC迴圈後,被移入舊域(演算法是標記-整理演算法)   

(3)永久域:儲存類和方法物件,從配置的角度看,這個域是獨立的,不包括在JVM堆內。預設為4M。   

       方法區(永久域):

       永久域的回收有兩種:常量池中的常量,無用的類資訊,常量的回收很簡單,沒有引用了就可以被回收。對於無用的類進行回收,必須保證3點:

  • 類的所有例項都已經被回收
  • 載入類的ClassLoader已經被回收
  • 類物件的Class物件沒有被引用(即沒有通過反射引用該類的地方)

     永久代的回收並不是必須的,可以通過引數來設定是否對類進行回收。

示例圖:

Gc 流程

  • 當eden滿了,觸發young GC;
  • young GC做2件事:一,去掉一部分沒用的object;二,把老的還被引用的object發到survior裡面,等下幾次GC以後,survivor再放到old裡面。
  • 當old滿了,觸發full GC。full GC很消耗記憶體,把old,young裡面大部分垃圾回收掉。這個時候使用者執行緒都會被block。

 1 簡述JVM垃圾回收機制  

     垃圾回收機制是Java提供的自動釋放記憶體空間的機制。   

    垃圾回收器(Garbage Collection,GC)是JVM自帶的一個執行緒,用於回收沒有被引用的物件。

2Java程式是否會出現記憶體洩露  

   會出現記憶體洩漏。   

   一般來說記憶體洩漏有兩種情況。一是在堆中分配的記憶體,在沒有將其釋放掉的時候,就將所有能訪問這塊記憶體的方式都刪掉;另一種情況則是在記憶體物件明明已經不需要的時候,還仍然保留著這塊記憶體和它的訪問方式(引用)。第一種情況,在Java中已經由於垃圾回收機制的引入,得到了很好的解決。所以,Java中的記憶體洩漏,主要指的是第二種情況。   

下面給出了一個簡單的記憶體洩露的例子。在這個例子中,我們迴圈申請Object物件,並將所申請的物件放入一個List中,如果我們僅僅釋放引用本身,那麼List仍然引用該物件,所以這個物件對GC來說是不可回收的。程式碼如下所示:

List list=new ArrayList(10);
for (int i=1;i<100; i++) 
{ 
     Object o=new Object(); 
     list.add(o); 
     o=null;
 }

 此時,所有的Object物件都沒有被釋放,因為變數list引用這些物件。

3 JVM如何管理記憶體,分成幾個部分?分別有什麼用途?說出下面程式碼的記憶體實現原理: Foo foo = new Foo(); foo.f();

JVM記憶體分為“堆”、“棧”和“方法區”三個區域,分別用於儲存不同的資料。   

堆記憶體用於儲存使用new關鍵字所建立的物件或陣列;

棧記憶體用於儲存程式執行時在方法中宣告的所有的區域性變數;

方法區用於存放類的資訊,Java程式執行時,首先會通過類裝載器載入類檔案的位元組碼資訊,經過解析後將其裝入方法區。類的各種資訊(包括方法)都在方法區儲存。

Foo foo = new Foo(); foo.f(); 以上程式碼的記憶體實現原理為:   

  1. Foo類首先被裝載到JVM的方法區,其中包括類的資訊,包括方法和構造等。   
  2. 在棧記憶體中分配引用變數foo。   
  3. 在堆記憶體中按照Foo型別資訊分配例項變數記憶體空間;然後,將棧中引用foo指向foo物件堆記憶體的首地址。   
  4. 使用引用foo呼叫方法,根據foo引用的型別Foo呼叫f方法。