天天看點

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

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

  Java虛拟機在執行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異常。

堆記憶體用來存放由new建立的對象執行個體和數組。(重點)

  Java堆是所有線程共享的一塊記憶體區域,在虛拟機啟動時建立,此記憶體區域的唯一目的就是存放對象執行個體 。

  Java堆是垃圾收集器管理的主要區域。由于現在收集器基本采用分代回收算法,是以Java堆還可細分為:新生代和老年代。從記憶體配置設定的角度來看,線程共享的Java堆中可能劃分出多個線程私有的配置設定緩沖區(TLAB)。

  Java堆可以處于實體上不連續的記憶體空間,隻要邏輯上連續的即可。在實作上,既可以實作固定大小的,也可以是擴充的。

  如果堆中沒有記憶體完成執行個體配置設定,并且堆也無法完成擴充時,将會抛出OutOfMemoryError異常。

   在棧記憶體中儲存的是堆記憶體空間的通路位址,或者說棧中的變量指向堆記憶體中的變量(Java中的指針)(重點)。

  Java棧是Java方法執行的記憶體模型每個方法在執行的同時都會建立一個棧幀的用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊。每個方法從調用直至執行完成的過程就對應着一個棧幀在虛拟機中入棧和出棧的過程。

  當在堆中産生了一個數組或者對象時,可以在棧中定義一個特殊的變量,讓棧中的這個變量的取值等于數組或對象在堆記憶體中的首位址,棧中的這個變量就成了數組或對象的引用變量,以後就可以在程式中使用棧中的引用變量來通路堆中的數組或者對象,引用變量就相當于是為數組或者對象起的一個名稱。引用變量是普通的變量,定義時在棧中配置設定,引用變量在程式運作到其作用域之外後被釋放。而數組和對象本身在堆中配置設定,即使程式運作到使用new産生數組或者對象的語句所在的代碼塊之外,數組和對象本身占據的記憶體不會被釋放,數組和對象在沒有引用變量指向它的時候,才變為垃圾,不能在被使用,但仍然占據記憶體空間不放,在随後的一個不确定的時間被垃圾回收器收走(釋放掉)。例如:

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

  由上圖我們知道,對象名稱p被儲存在了棧記憶體中,具體執行個體儲存在堆記憶體中。也就是說,在棧記憶體中儲存的是堆記憶體空間的通路位址,或者說棧中的變量指向堆記憶體中的變量(Java中的指針)。

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

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

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

  方法區是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料 (重點)。

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

  當方法區無法滿足記憶體配置設定需要時,将抛出OutOfMemoryError異常。

  運作時常量池:

   是方法區的一部分,它用于存放編譯期生成的各種字面量和符号引用。

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

接下來探讨以hotspot虛拟機在Java堆中對象配置設定、布局和通路的全過程。

  建立一個對象通常是需要new關鍵字,當虛拟機遇到一條new指令時,首先檢查這個指令的參數是否在常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果那麼執行相應的類加載過程。

  類加載檢查通過後,虛拟機将為新生對象配置設定記憶體。為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來。

  配置設定的方式有兩種:

   一種叫 指針碰撞 ,假設Java堆中記憶體是絕對規整的,用過的和空閑的記憶體各在一邊,中間放着一個指針作為分界點的訓示器,配置設定記憶體就是把那個指針向空閑空間的那邊挪動一段與對象大小相等的距離。

   另一種叫 空閑清單 :如果Java堆中的記憶體不是規整的,虛拟機就需要維護一個清單,記錄哪個記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄。

  采用哪種配置設定方式是由*Java堆是否規整決定的,而Java堆是否規整是由所采用的垃圾收集器是否帶有壓縮整理功能決定的。 另 外一個需要考慮的問題就是對象建立時的線程安全問題*,有兩種解決方案:一是對配置設定記憶體空間的動作進行同步處理;另一種是吧記憶體配置設定的動作按照線程劃分在不 同的空間之中進行,即每個線程在Java堆中預先配置設定一小塊記憶體(TLAB),哪個線程要配置設定記憶體就在哪個線程的TLAB上配置設定,隻有TLAB用完并配置設定 新的TLAB時才需要同步鎖定。

  記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間初始化為零值。這一步操作保證了對象的執行個體字段在Java代碼中可以不賦初始值就可以直接使用。

  接下來虛拟機要對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊等,這些資訊存放在對象的對象頭中。

  上面的工作都完成以後,從虛拟機的角度來看一個新的對象已經産生了。但是從Java程式的角度,還需要執行init方法,把對象按照程式員的意願進行初始化,這樣一個真正可用的對象才算完全産生出來。

執行過程如圖:(這是簡化過程)

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

  在HotSpot虛拟機中,對象在記憶體中存儲的布局可分為三個部分: 對象頭、執行個體資料和對齊填充。

  對象頭包括兩個部分:第一部分用于存儲對象自身的運作時資料,如哈希碼、GC分代年齡、線程所持有的鎖等。官方稱之為“Mark Word”。第二個部分為是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。

  執行個體資料是對象真正存儲的有效資訊,也是程式代碼中所定義的各種類型的字段内容。

  對齊填充并不是必然存在的,僅僅起着占位符的作用。、Hotpot VM要求對象起始位址必須是8位元組的整數倍,對象頭部分正好是8位元組的倍數,是以當執行個體資料部分沒有對齊時,需要通過對齊填充來對齊。

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

  Java程式通過棧上的reference資料來操作堆上的具體對象。目前主流的通路方式由“使用句柄”和“直接指針”。

  如果使用句柄通路的話,Java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料的具體各自的位址資訊。如下圖:

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

  如果使用直接指針通路的話,Java堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,reference中存儲的直接就是對象位址,如下圖:

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

對比

  使用句柄來通路的最大好處就是reference中存儲的是穩定句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要被修改。

  使用直接指針來通路最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象通路的在Java中非常頻繁,是以這類開銷積小成多也是一項非常可觀的執行成本。

  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對象沒有被引用(即沒有通過反射引用該類的地方)

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

示例圖:

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

當eden滿了,觸發young GC;

young GC做2件事:一,去掉一部分沒用的object;二,把老的還被引用的object發到survior裡面,等下幾次GC以後,survivor再放到old裡面。

當old滿了,觸發full GC。full GC很消耗記憶體,把old,young裡面大部分垃圾回收掉。這個時候使用者線程都會被block。

再具體舊不會了,大概了解下它的流程就好了,在向下了解就需要更深的知識現在就不分析了,等以後會了我會在寫寫關于這方面的

下面分享幾個簡單的gc題

1 簡述JVM垃圾回收機制

 參考答案

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

  垃圾回收器(Garbage Collection,GC)是JVM自帶的一個線程,用于回收沒有被引用的對象。

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

  會出現記憶體洩漏。

  一般來說記憶體洩漏有兩種情況。一是在堆中配置設定的記憶體,在沒有将其釋放掉的時候,就将所有能通路這塊記憶體的方式都删掉;另一種情況則是在記憶體對象明明已經不需要的時候,還仍然保留着這塊記憶體和它的通路方式(引用)。第一種情況,在Java中已經由于垃圾回收機制的引入,得到了很好的解決。是以,Java中的記憶體洩漏,主要指的是第二種情況。

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

  此時,所有的Object對象都沒有被釋放,因為變量list引用這些對象。

3 JVM如何管理記憶體,分成幾個部分?分别有什麼用途?說出下面代碼的記憶體實作原理:

Foo foo = new Foo();

foo.f();

參考答案

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

  堆記憶體用于存儲使用new關鍵字所建立的對象;棧記憶體用于存儲程式運作時在方法中聲明的所有的局部變量;方法區用于存放類的資訊,Java程式運作時,首先會通過類裝載器載入類檔案的位元組碼資訊,經過解析後将其裝入方法區。類的各種資訊(包括方法)都在方法區存儲。

以上代碼的記憶體實作原理為:

  1.Foo類首先被裝載到JVM的方法區,其中包括類的資訊,包括方法和構造等。

  2.在棧記憶體中配置設定引用變量foo。

  3.在堆記憶體中按照Foo類型資訊配置設定執行個體變量記憶體空間;然後,将棧中引用foo指向foo對象堆記憶體的首位址。

  4.使用引用foo調用方法,根據foo引用的類型Foo調用f方法。