原文轉載自:http://my.oschina.net/sunchp/blog/369707java
1.JVM內存模型程序員
JVM運行時內存=共享內存區+線程內存區算法
1).共享內存區windows
共享內存區=持久帶+堆數組
持久帶=方法區+其餘多線程
堆=Old Space+Young Space
併發
Young Space=Eden+S0+S1oracle
(1)持久帶jvm
JVM用持久帶(Permanent Space)實現方法區,主要存放全部已加載的類信息,方法信息,常量池等等。xss
可經過-XX:PermSize和-XX:MaxPermSize來指定持久帶初始化值和最大值。
Permanent Space並不等同於方法區,只不過是Hotspot JVM用Permanent Space來實現方法區而已,有些虛擬機沒有Permanent Space而用其餘機制來實現方法區。
(2)堆
堆,主要用來存放類的對象實例信息。
堆分爲Old Space(又名,Tenured Generation)和Young Space。
Old Space主要存放應用程序中生命週期長的存活對象;
Eden(伊甸園)主要存放新生的對象;
S0和S1是兩個大小相同的內存區域,主要存放每次垃圾回收後Eden存活的對象,做爲對象從Eden過渡到Old Space的緩衝地帶(S是指英文單詞Survivor Space)。
堆之因此要劃分區間,是爲了方便對象建立和垃圾回收,後面垃圾回收部分會解釋。
2).線程內存區
線程內存區=單個線程內存+單個線程內存+.......
單個線程內存=PC Regster+JVM棧+本地方法棧
JVM棧=棧幀+棧幀+.....
棧幀=局域變量區+操做數區+幀數據區
在Java中,一個線程會對應一個JVM棧(JVM Stack),JVM棧裏記錄了線程的運行狀態。
JVM棧以棧幀爲單位組成,一個棧幀表明一個方法調用。棧幀由三部分組成:局部變量區、操做數棧、幀數據區。
(1)局部變量區
局部變量區,能夠理解爲一個以數組形式進行管理的內存區,從0開始計數,每一個局部變量的空間是32位的,即4字節。
基本類型byte、char、short、boolean、int、float及對象引用等佔一個局部變量空間,類型爲short、byte和char的值在存入數組前要被轉換成int值;long、double佔兩個局部變量空間,在訪問long和double類型的局部變量時,只須要取第一個變量空間的索引便可,。
例如:
public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) { return 0; } public int runInstanceMethod(char c,double d,short s,boolean b) { return 0; }
runInstanceMethod的局部變量區第一項是個reference(引用),它指定的就是對象自己的引用,也就是咱們經常使用的this,可是在runClassMethod方法中,沒這個引用,那是由於runClassMethod是個靜態方法。
(2)操做數棧
操做數棧和局部變量區同樣,也被組織成一個以字長爲單位的數組。但和前者不一樣的是,它不是經過索引來訪問的,而是經過入棧和出棧來訪問的。操做數棧是臨時數據的存儲區域。
例如:
int a= 100; int b =5; int c = a+b;
對應的操做數棧變化爲:
從圖中能夠得出:操做數棧其實就是個臨時數據存儲區域,它是經過入棧和出棧來進行操做的。
PS:JVM實現裏,有一種基於棧的指令集(Hotspot,oracle JVM),還有一種基於寄存器的指令集(DalvikVM,安卓 JVM),二者有什麼區別的呢?
基於棧的指令集有接入簡單、硬件無關性、代碼緊湊、棧上分配無需考慮物理的空間分配等優點,可是因爲相同的操做須要更多的出入棧操做,所以消耗的內存更大。 而基於寄存器的指令集最大的好處就是指令少,速度快,可是操做相對繁瑣。
示例:
public class Test { public static void foo() { int a = 1; int b = 2; int c = (a + b) * 5; } public static void main(String[] args) { foo(); } }
基於棧的Hotspot的執行過程以下:
基於寄存器的DalvikVM執行過程以下所示:
上述兩種方式最終經過JVM執行引擎,CPU接收到的彙編指令是:
(3)幀數據區
幀數據區存放了指向常量池的指針地址,當某些指令須要得到常量池的數據時,經過幀數據區中的指針地址來訪問常量池的數據。此外,幀數據區還存放方法正常返回和異常終止須要的一些數據。
2.垃圾回收機制
1)、爲何要垃圾回收
JVM自動檢測和釋放再也不使用的內存,提升內存利用率。
Java 運行時JVM會執行 GC,這樣程序員再也不須要顯式釋放對象。
2)、回收哪些內存區域
由於線程內存區隨着線程的產生和退出而分配和回收,因此垃圾回收主要集中在共享內存區,也就是持久帶(Permanent Space)和堆(Heap)。
3)、如何判斷對象已死 (對象標記)
(1)引用計數法
引用計數法就是經過一個計數器記錄該對象被引用的次數,方法簡單高效,可是解決不了循環引用的問題。好比對象A包含指向對象B的引用,對象B也包含指向對象A的引用,但沒有引用指向A和B,這時當前回收若是採用的是引用計數法,那麼對象A和B的被引用次數都爲1,都不會被回收。JVM不是採用這種方法。
(2) 根搜索(可達性分析算法)
根搜索(可達性分析算法)能夠解決對象循環引用的問題,基本原理是:經過一個叫「GC ROOT」根對象做爲起點,而後根據關聯關係,向下節點搜索,搜索路徑叫引用鏈,也就是常說的引用。從「GC ROOT」根對象找不到任何一條路徑與之相連的對象,就被斷定能夠回收,至關於這對象找不到家的感受。
示例圖:
GC會收集那些不是GC root且沒有被GC root引用的對象。一個對象能夠屬於多個GC root。
GC root有幾下種:
虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI(native方法)引用的對象
用於JVM特殊目的對象,例如系統類加載器等等
雖然有可達性分析算法來斷定對象狀態,但這並非對象是否被回收的條件,對象回收的條件遠遠比這個複雜。沒法經過GC ROOT關聯到的對象,不都是馬上被回收。若是這個對象沒有被關聯到,並且沒有被mark2標記,那麼會進入一個死緩的階段,被第一次標記(mark1),而後被放入一個F-Queue隊列;若是這個對象被mark2標記了,那麼這個對象將會被回收。
F-Queue隊列由一個優先級較低的Finalizer線程去執行,其中的mark1對象等待執行本身的finalize()方法(JVM並不保證等待finalize()方法運行結束,由於finalize() 方法或者執行慢,或者死循環,會影響該隊列其餘元素執行)。執行mark1對象的finalize()方法,就會進行第二次標記(mark2)。之後的GC都會按這個邏輯執行「搜索,標記1,標記2」。
這一「標記」過程是後續垃圾回收算法的基礎。
PS:
若是在finalize() 方法體內,再次對本對象進行引用,那麼對象就復活了。
finalize()方法只會被執行一次,因此對象只有一次復活的機會。
3)垃圾回收算法
垃圾回收算法主要有三種:
標記-清除
標記-複製
標記-整理
這三種都有「標記」過程,這個標記過程就是上述的根搜索(可達性分析算法)。後面的「清除」、「複製」和「整理」動做,是具體的對象被回收的實現方式。
(1)標記-清除
經過根搜索(可達性分析算法)標記完成後,直接將標記爲垃圾的對象所佔內存空間釋放。這種算法的缺點是內存碎片多。
雖然缺點明顯,這種策略倒是後兩種策略的基礎。正由於它的缺點,因此促成了後兩種策略的產生。
動圖:
(2)標記-複製
經過根搜索(可達性分析算法)標記完成後,將內存分爲兩塊,將一塊內存中保留的對象所有複製到另
一塊空閒內存中。
動圖:
這種算法的缺點是,可用內存變成了一半。怎麼解決這個缺點呢?
JVM將堆(heap)分紅young區和old區。young區包括eden、s0、s1,而且三個區之間的大小有必定比例。例如,按8:1:1分紅一塊Eden和兩小塊Survivor區,每次GC時,young區裏,將Eden和S0中存活的對象複製到另外一塊空閒的S1中。
young區的垃圾回收是常常要發生的,被稱爲Minor GC(次要回收)。通常狀況下,當新對象生成,而且在Eden申請空間失敗時,就會觸發Minor GC,對Eden區域進行GC,清除非存活對象,而且把尚且存活的對象移動到Survivor區。而後整理Survivor的兩個區。這種方式的GC是對Young space的Eden區進行,不會影響到Old space。由於大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,因此Eden區的GC會頻繁進行。於是,通常在這裏須要使用速度快、效率高的算法,使Eden去能儘快空閒出來。
Minor GC主要過程:
a、新生成的對象在Eden區完成內存分配;
b、當Eden區滿了,再建立對象,會由於申請不到空間,觸發minorGC,進行young(eden+1survivor)區的垃圾回收。(爲何是eden+1survivor:兩個survivor中始終有一個survivor是空的,空的那個被標記成To Survivor);
c、minorGC時,Eden不能被回收的對象被放入到空的survivor(也就是放到To Survivor,同時Eden確定會被清空),另外一個survivor(From Survivor)裏不能被GC回收的對象也會被放入這個survivor(To Survivor),始終保證一個survivor是空的。(MinorGC完成以後,To Survivor 和 From Survivor的標記互換);
d、當作第3步的時候,若是發現存放對象的那個survivor滿了,則這些對象被copy到old區,或者survivor區沒有滿,可是有些對象已經足夠Old(經過XX:MaxTenuringThreshold參數來設置),也被放入Old區。(對象在Survivor區中每熬過一次Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲)時,就會晉升到老年代中)
(3)標記-整理
old space也能夠標記-複製策略嗎?固然不行!
young space中的對象大部分都是生命週期較短的對象,每次GC後,所剩下的活對象數量不是很大。而old space中的對象大部分都是生命週期特別長的對象,即便GC後,仍然會剩下大量的活對象。若是仍然採用複製動做,回收效率會變得很是低。
根據old space的特色,能夠採用整理動做。整理時,先清除掉應該清除的對象,而後把存活對象「壓縮」到堆的一端,按順序排放。
動圖:
Old space(+Permanent Space)的垃圾回收是偶爾發生的,被稱爲Full GC(主要回收)。Full GC由於須要對整個堆進行回收,包括Young、Old和Perm,因此比Minor GC要慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於FullGC的調節。
有以下緣由可能致使Full GC:
年老代(Tenured)被寫滿
持久代(Perm)被寫滿
System.gc()被顯示調用
上一次GC以後Heap的各域分配策略動態變化
4)、垃圾收集器
垃圾收集算法是內存回收的理論基礎,而垃圾收集器就是內存回收的具體實現。
堆(Heap)分代被目前大部分JVM所採用。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲old space和Young space,old space的特色是每次垃圾收集時只有少許對象須要被回收,而Young space的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。
目前大部分垃圾收集器對於Young space都採起「標記-複製」算法。而因爲Old space的特色是每次回收都只回收少許對象,通常使用的是「標記-整理」算法。
(1)Young Space上的GC實現:
Serial(串行): Serial收集器是最基本最古老的收集器,它是一個單線程收集器,而且在它進行垃圾收集時,必須暫停全部用戶線程。Serial收集器是針對新生代的收集器,採用的是「標記-複製」算法。它的優勢是實現簡單高效,可是缺點是會給用戶帶來停頓。這個收集器類型僅應用於單核CPU桌面電腦。使用serial收集器會顯着下降應用程序的性能。
ParNew(並行): ParNew收集器是Serial收集器的多線程版本,使用多個線程進行垃圾收集。
Parallel Scavenge(並行): Parallel Scavenge收集器是一個新生代的多線程收集器(並行收集器),它在回收期間不須要暫停其餘用戶線程,其採用的是「標記-複製」算法,該收集器與前兩個收集器有所不一樣,它主要是爲了達到一個可控的吞吐量。
(2)Old Space上的GC實現:
Serial Old(串行):Serial收集器的Old Space版本,採用的是「標記-整理」算法。這個收集器類型僅應用於單核CPU桌面電腦。使用serial收集器會顯着下降應用程序的性能。
Parallel Old(並行):Parallel Old是Parallel Scavenge收集器的Old Space版本(並行收集器),使用多線程和「標記-整理」算法。
CMS(併發):CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,它是一種併發收集器,採用的是"標記-清除"算法。
(3).G1
G1(Garbage First)收集器是JDK1.7提供的一個新收集器,G1收集器基於「標記-整理」算法實現,也就是說不會產生內存碎片。還有一個特色以前的收集器進行收集的範圍都是整個新生代或老年代,而G1將整個Java堆(包括新生代,老年代)。
3.JVM參數
1).堆
-Xmx:最大堆內存,如:-Xmx512m
-Xms:初始時堆內存,如:-Xms256m
-XX:MaxNewSize:最大年輕區內存
-XX:NewSize:初始時年輕區內存.一般爲 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間爲 = Eden + 1 個 Survivor,即 90%
-XX:MaxPermSize:最大持久帶內存
-XX:PermSize:初始時持久帶內存
-XX:+PrintGCDetails。打印 GC 信息
-XX:NewRatio 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3
-XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。默認值爲 8。即 Eden 佔新生代空間的 8/10,另外兩個 Survivor 各佔 1/10
2).棧
-xss:設置每一個線程的堆棧大小. JDK1.5+ 每一個線程堆棧大小爲 1M,通常來講若是棧不是很深的話, 1M 是絕對夠用了的。
3).垃圾回收
4).JVM client模式和server模式
Java_home/bin/java命令有一個-server和-client參數,該參數標識了JVM以server模式或client模式啓動。
JVM Server模式與client模式啓動,最主要的差異在於:-Server模式啓動時,速度較慢,可是一旦運行起來後,性能將會有很大的提高。當虛擬機運行在-client模式的時候,使用的是一個代號爲C1的輕量級編譯器, 而-server模式啓動的虛擬機採用相對重量級,代號爲C2的編譯器. C2比C1編譯器編譯的相對完全,,服務起來以後,性能更高。
(1)查看當前JVM默認啓動模式
java -version 能夠直接查看出默認使用的是client仍是 server。
(2)JVM默認啓動模式自動偵測
從JDK 5開始,若是沒有顯式地用-client或者-server參數,那麼JVM啓動時,會根據機器配置和JDK的版本,自動判斷該用哪一種模式。
the definition of a server-class machine is one with at least 2 CPUs and at least 2GB of physical memory.
windows平臺,64位版本的JDK,沒有提供-client模式,直接使用server模式。
(3).經過配置文件,改變JVM啓動模式
兩種模式的切換能夠經過更改配置(jvm.cfg配置文件)來實現:
32位的JVM配置文件在JAVA_HOME/jre/lib/i386/jvm.cfg,
64位的JVM配置文件在JAVA_HOME/jre/lib/amd64/jvm.cfg, 目前64位只支持server模式。
例如:
32位版本的JDK 5的jvm.cfg文件內容:
-client KNOWN -server KNOWN -hotspot ALIASED_TO -client -classic WARN -native ERROR -green ERROR
64位版本的JDK 7的jvm.cfg文件內容:
-server KNOWN -client IGNORE -hotspot ALIASED_TO -server -classic WARN -native ERROR -green ERROR
4.堆 VS 棧
JVM棧是運行時的單位,而JVM堆是存儲的單位。
JVM棧表明了處理邏輯,而JVM堆表明了數據。
JVM堆中存的是對象。JVM棧中存的是基本數據類型和JVM堆中對象的引用。
JVM堆是全部線程共享,JVM棧是線程獨有。
PS:Java中的參數傳遞是傳值呢?仍是傳址?
咱們都知道:C 語言中函數參數的傳遞有:值傳遞,地址傳遞,引用傳遞這三種形式。可是在Java裏,方法的參數傳遞方式只有一種:值傳遞。所謂值傳遞,就是將實際參數值的副本(複製品)傳入方法內,而參數自己不會受到任何影響。
要說明這個問題,先要明確兩點:
1.引用在Java中是一種數據類型,跟基本類型int等等同一地位。
2.程序運行永遠都是在JVM棧中進行的,於是參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象自己。
在運行JVM棧中,基本類型和引用的處理是同樣的,都是傳值。若是是傳引用的方法調用,能夠理解爲「傳引用值」的傳值調用,即「引用值」被作了一個複製品,而後賦值給參數,引用的處理跟基本類型是徹底同樣的。可是當進入被調用方法時,被傳遞的這個引用值,被程序解釋(或者查找)到JVM堆中的對象,這個時候纔對應到真正的對象。若是此時進行修改,修改的是引用對應的對象,而不是引用自己,即:修改的是JVM堆中的數據。因此這個修改是能夠保持的了。
例如:
public class DataWrap { public int a; public int b; } public class ReferenceTransferTest { public static void swap(DataWrap dw) { int tmp = dw.a; dw.a = dw.b; dw.b = tmp; } public static void main(String[] args) { DataWrap dw = new DataWrap(); dw.a = 6; dw.b = 9; swap(dw); } }
對應的內存圖:
附: