JVM是Java程序運行的環境,但是他同時也是一個操作系統的一個應用程序的一個進程,因此JVM也有他自己的運行生命周期,也有自己的代碼和數據空間。
JDK
JDK在Java的整個體系中充當一個生產加工中心,產生所有的數據輸出,是所有指令和戰略的執行中心。本身還提供了Java的完整方案,可以開發目前Java能支持的所有應用和系統程序。而之所以現在還會分j2me,j2ee這些類,是把他們用來簡化各自領域內的開發和構建過程。JDK除了JVM之外,還有一些核心的API,用戶工具,技術等等。
而JVM在JDK中就處於最底層的位置,負責操作系統的交互,用來屏蔽操作系統環境,提供一個完整的Java運行環境,也就是一個虛擬計算機。
GC(垃圾回收)
Java堆的描述如下:
內存由Perm和Heap組成。
JVM內存模型中分兩大塊,其實在垃圾回收的算法中,有一個方法就是分代垃圾回收(這個在下面的時候我會歸納一下)。而JVM這里也就是一個分代的原理。一塊是新生代,一塊是老一代。在老一代里面,存放的東西都是應用程序生命周期較長的內存對象(老一代嘛),而新生代里面存放東西的生命周期就要短一些了(因此在垃圾回收算法中可以根據不同代的特點來指定不同的回收方案)。
在新生代中,有一個叫Eden(聖經中伊甸園的意思,這樣看名字就可以猜出它大概的意思及作用了吧)的空間來存放新生的對象,還有兩個Survivor Spaces它們是用來存放每次垃圾回收后存活下來的對象。
還有個Permanent Generation Space, 是指內存的永久保存區域,而一般出現OOM的時候,就是內存溢出了。而為什么會溢出呢?是因為Class在被Load的時候被放入該區域,它和存放Instance的Heap區域不同,GC不會在主程序運行期對PermGen space進行清理,所以如果你的APP會LOAD很多CLASS的話,就很可能出現PermGen space錯誤。
它其實就是一個方法區,里面主要存放了兩種信息。
1.Class的節本信息
Package Name
Super class package name
Class or interface
Type modifiers
Super inferface package name
2.其它信息
The constant pool for the type
Field information
Method information
All class (static) variables declared
in the type, except constants
A reference to class ClassLoader
A reference to class Class
常見的溢出
-
OLD段溢出
這種內存溢出是最常見的情況之一,產生的原因可能是:
1) 設置的內存參數過小(ms/mx, NewSize/MaxNewSize)
2) 程序問題
單個程序持續進行消耗內存的處理,如循環幾千次的字符串處理,對字符串處理應建議使用StringBuffer。此時不會報內存溢出錯,卻會使系統持續垃圾收集,無法處理其它請求,單個程序所申請內存過大,有的程序會申請幾十乃至幾百兆內存,此時JVM也會因無法申請到資源而出現內存溢出。
當Java對象使用完畢后,其所引用的對象卻沒有銷毀,使得JVM認為他還是活躍的對象而不進行回收,這樣累計占用了大量內存而無法釋放。 -
Perm段溢出
通常由於Perm段裝載了大量的類而導致溢出,目前的解決辦法:就是增加它的空間。 -
C Heap溢出
系統對C Heap沒有限制,故C Heap發生問題時,Java進程所占內存會持續增長,直到占用所有可用系統內存 - 其他:
JVM有2個GC線程。第一個線程負責回收Heap的NEW區。第二個線程在Heap不足時,遍歷Heap,將NEW區升級為Older區。Older區的大小等於-Xmx減去-Xmn,不能將-Xms的值設的過大,因為第二個線程被迫運行會降低JVM的性能。
為什么一些程序頻繁發生GC?有如下原因:
1. 程序內調用了System.gc()或Runtime.gc()。
2. 一些中間件軟件調用自己的GC方法,此時需要設置參數禁止這些GC。
3. Java的Heap太小,一般默認的Heap值都很小。
4. 頻繁實例化對象,Release對象。此時盡量保存並重用對象,例如使用StringBuffer和String。
如果每次GC后,Heap的剩余空間會是總空間的50%,這表示你的Heap處於健康狀態。許多Server端的Java程序每次GC后最好能有65%的剩余空間。
注意:
- 增加Heap的大小雖然會降低GC的頻率,但也增加了每次GC的時間。並且GC運行時,所有的用戶線程將暫停,也就是GC期間,Java應用程序不做任何工作。
- Heap大小並不決定進程的內存使用量。進程的內存使用量要大於-Xmx定義的值,因為Java為其他任務分配內存,例如每個線程的Stack等。
- 每個線程都有他自己的Stack,Stack的大小限制着線程的數量。如果Stack過大就好導致內存溢漏
- 硬件環境也影響GC的效率,例如機器的種類,內存,swap空間,和CPU的數量。如果你的程序需要頻繁創建很多transient對象,會導致JVM頻繁GC。這種情況你可以增加機器的內存,來減少Swap空間的使用
垃圾回收
在新生代塊中,垃圾回收一般用Copying的算法,速度快。每次GC的時候,存活下來的對象首先由Eden拷貝到某個Survivor Space, 當Survivor Space空間滿了后, 剩下的live對象就被直接拷貝到老一代中。因此,每次GC后,Eden內存塊會被清空。在老一代中,垃圾回收一般用mark-compact(標記-整理)算法,它的內存占用少,速度慢。
垃圾回收分多級,0級為全部(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上為部分垃圾回收,只會回收NEW中的垃圾,內存溢出通常發生於OLD段或Perm段垃圾回收后,仍然無內存空間容納新的Java對象的情況。
什么情況下觸發垃圾回收
由於對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種類型:Scavenge GC 和 Full GC
Scavenge GC
一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然后整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到老一代。因為大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這里需要使用速度快、效率高的算法,使Eden區能盡快空閑出來。
Full GC
對整個堆進行整理,包括年輕代,老一代和持久代。Full GC 因為需要對整個堆進行回收,所以比 Scavenge GC 要慢,因此應該盡可能減少 Full GC 的次數。在對JVM調優的過程中,很大一部分工作就是對於 Full GC 的調節。
有如下原因可能導致Full GC:
. 老一代被寫滿
. 持久代被寫滿
. System.gc()被顯式調用
. 上一次GC之后Heap的各域分配策略動態變化
JVM如何判斷一個對象為垃圾
這個時候就要考慮JVM什么時候才把一個對象當成垃圾了,常用的有以下幾種方法:
引用計數器算法
定義: 引用計數器算法是給每個對象設置一個計數器,當有地方引用這個對象的時候,計數器+1,當引用失效的時候,計數器-1,當計數器為0的時候,JVM就認為對象不再被使用,是“垃圾”了。
優缺點:引用計數器實現簡單,效率高;但是不能解決循環引用問問題(A對象引用B對象,B對象又引用A對象,但是A,B對象已不被任何其他對象引用),同時每次計數器的增加和減少都帶來了很多額外的開銷,所以在JDK1.1之后,這個算法已經不再使用了。
根搜索方法
根搜索方法是通過一些“GC Roots”對象作為起點,從這些節點開始往下搜索,搜索通過的路徑成為引用鏈(Reference Chain),當一個對象沒有被GC Roots的引用鏈連接的時候,說明這個對象是不可用的。
GC Roots對象包括:
a) 虛擬機棧(棧幀中的本地變量表)中的引用的對象。
b) 方法區域中的類靜態屬性引用的對象。
c) 方法區域中常量引用的對象。
d) 本地方法棧中JNI(Native方法)的引用的對象。
垃圾回收算法
一、按回收策略來分可分為三種
標記-清除法(Mark-Sweep)
標記清除法分為兩個階段,一個是標記,另一個是清除。
在標記階段,確定所有要回收的對象,並作標記
在清除階段,將所有標記了的對象清除。它的操作是緊跟標記階段的
缺點:
標記和清除階段的效率不高,而且清除后回產生大量的不連續空間,這樣當程序需要分配大內存對象時,可能無法找到足夠的連續空間。
復制算法(Coping)
復制算法是把內存分為大小相等的兩塊,每次使用其中的一塊,當垃圾回收的時候,把存貨的對象復制到另一塊上,然后把這段內存清除掉。
復制算法實現簡單,運行效率高,但是由於每次只能使用其中的一半,造成內存的利用率不高。現在的JVM用復制方法收集新生代,由於新生代中大部分對象(98%)都是朝生夕死的,所以兩塊內存的比例不是1:1(大概是8:1)。復制算法完成后會形成連續的空間。
標記整理算法(Mark-Compact)
標記-整理算法和標記-清除算法一樣,但是標記-整理算法是把存活的對象直接向內存的一端移動,然后把超過邊界的內存直接清除。
標記整理算法提高了內存的利用率,適用於收集存貨時間較長的老一代。這種算法完成之后也會是連續的內存空間
二、按分區對待的方式來分可分為兩種
分代收集
這個就是根據對象的存活時間,分為老一代(存活時間上)和新一代(存活時間短),然后根據不同的年代來采用不同的算法。老一代采用標記整理算法,新一代采用復制算法。
在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鈎,因此生命周期比較長。
但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如:String對象,由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。
增量收集
它又被稱為實時垃圾回收算法。即:在應用進行的同時進行垃圾回收。
三、按系統線程可分為三種
串行收集
串行收集使用單線程處理所有垃圾回收工作,因為無需多線程交互,實現容易,而且效率比較高。但是,其局限性也比較明顯,即無法使用多處理器的優勢,所以此收集適合單處理器機器。當然,此收集器也可以用在小數據量(100M左右)情況下的多處理器機器上。
並行收集
並行收集使用多線程處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能體現出並行收集器的優勢。(串型收集的並發版本,需要暫停jvm) 並行paralise指的是多個任務在多個cpu中一起並行執行,最后將結果合並。效率是N倍。
並發收集
相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個運行環境,而只有垃圾回收程序在運行,因此,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因為堆越大而越長。(和並行收集不同,並發只有在開頭和結尾會暫停jvm)並發concurrent指的是多個任務在一個cpu偽同步執行,但其實是串行調度的,效率並非直接是N倍。
四種GC
第一種為單線程GC,也是默認的GC,適用於單CPU的機器
第二種為多線程GC,適用於多CPU,使用大量線程的程序。這跟第一種是類似的,這種GC在回收NEW區時是多線程的,但是在回收OLD區是跟第一種一樣的,仍然采用單線程。
第三種為Concurrent Low Pause GC,類似於第一種,適用於多CPU,並要求縮短因GC造成程序停滯的時間。這種GC可以在Old區的回收同時,運行應用程序
第四種第四種為Incremental Low Pause GC,適用於要求縮短因GC造成程序停滯的時間。這種GC可以在Young區回收的同時,回收一部分Old區對象。
JVM操作cpu與內存交互的工作原理
在C/C++中,它們的工作原理是
先將語句轉化為匯編,
再把匯編轉換為二進制數據傳給CPU,
cpu通過控制總線來控制cpu的地址總線尋找內存地址,數據總線傳送數據到內存單元中。獲得數據實現與內存的交互。
而在Java這一塊來說的話,編譯成.class文件(它是一個字節碼文件)之后,這個時候cpu就相當於可以和他進行交互了,而jvm就負責在中間這一塊去識別它。如果照上面的C/C++的邏輯來說,.class可以理解為cpu可以理解的語言了(JVM負責翻譯)。
