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異常。
運行時常量池:
是方法區的一部分,它用於存放編譯期生成的各種字面量和符號引用。

對象管理機制
接下來探討以hotspot虛擬機在Java堆中對象分配、布局和訪問的全過程。
對象創建
創建一個對象通常是需要new關鍵字,當虛擬機遇到一條new指令時,首先檢查這個指令的參數是否在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果那么執行相應的類加載過程。
類加載檢查通過后,虛擬機將為新生對象分配內存。為對象分配空間的任務等同於把一塊確定大小的內存從Java堆中划分出來。
分配的方式有兩種:
一種叫 指針碰撞 ,假設Java堆中內存是絕對規整的,用過的和空閑的內存各在一邊,中間放着一個指針作為分界點的指示器,分配內存就是把那個指針向空閑空間的那邊挪動一段與對象大小相等的距離。
另一種叫 空閑列表 :如果Java堆中的內存不是規整的,虛擬機就需要維護一個列表,記錄哪個內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄。
采用哪種分配方式是由Java堆是否規整決定的,而Java堆是否規整是由所采用的垃圾收集器是否帶有壓縮整理功能決定的。 另 外一個需要考慮的問題就是對象創建時的線程安全問題,有兩種解決方案:一是對分配內存空間的動作進行同步處理;另一種是吧內存分配的動作按照線程划分在不 同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存(TLAB),哪個線程要分配內存就在哪個線程的TLAB上分配,只有TLAB用完並分配 新的TLAB時才需要同步鎖定。
內存分配完成后,虛擬機需要將分配到的內存空間初始化為零值。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就可以直接使用。
接下來虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息等,這些信息存放在對象的對象頭中。
上面的工作都完成以后,從虛擬機的角度來看一個新的對象已經產生了。但是從Java程序的角度,還需要執行init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象才算完全產生出來。
執行過程如圖:(這是簡化過程)

對象的內存布局
在HotSpot虛擬機中,對象在內存中存儲的布局可分為三個部分: 對象頭、實例數據和對齊填充。
對象頭包括兩個部分:第一部分用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、線程所持有的鎖等。官方稱之為“Mark Word”。第二個部分為是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
實例數據是對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段內容。
對齊填充並不是必然存在的,僅僅起着占位符的作用。、Hotpot VM要求對象起始地址必須是8字節的整數倍,對象頭部分正好是8字節的倍數,所以當實例數據部分沒有對齊時,需要通過對齊填充來對齊。

對象的訪問定位
Java程序通過棧上的reference數據來操作堆上的具體對象。目前主流的訪問方式由“使用句柄”和“直接指針”。
如果使用句柄訪問的話,Java堆中將會划分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據的具體各自的地址信息。如下圖:

如果使用直接指針訪問的話,Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,如下圖:

對比
使用句柄來訪問的最大好處就是reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要被修改。
使用直接指針來訪問最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象訪問的在Java中非常頻繁,因此這類開銷積小成多也是一項非常可觀的執行成本。
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。
再具體舊不會了,大概理解下它的流程就好了,在向下理解就需要更深的知識現在就不分析了,等以后會了我會在寫寫關於這方面的
下面分享幾個簡單的gc題
1 簡述JVM垃圾回收機制
參考答案
垃圾回收機制是Java提供的自動釋放內存空間的機制。
垃圾回收器(Garbage Collection,GC)是JVM自帶的一個線程,用於回收沒有被引用的對象。
2 Java程序是否會出現內存泄露
參考答案
會出現內存泄漏。
一般來說內存泄漏有兩種情況。一是在堆中分配的內存,在沒有將其釋放掉的時候,就將所有能訪問這塊內存的方式都刪掉;另一種情況則是在內存對象明明已經不需要的時候,還仍然保留着這塊內存和它的訪問方式(引用)。第一種情況,在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方法。
