JDK (Java Development Kit) : java語言的軟件開發包。包括Java運行時環境JRE。
JRE (Java Runtime Environment) :Java運行時環境,包括JVM。
JVM (Java Virtual Machine) :一種用于計算機設備的規范。
Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。
實現通過類的權限定名獲取該類的二進制字節流的代碼塊叫做類加載器。
啟動類加載器(Bootstrap ClassLoader)用來加載 java 核心類庫,無法被 java 程序直接引用。
擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。
系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH) 來加載 Java類。一般來說,Java應用的類都是由它來完成加載的。可以通過ClassLoader.getSystemClassLoader()來獲取它。
用戶自定義類加載器,通過繼承 java.lang.ClassLoader 類的方式實現。
ClassLoader 顧名思義就是類加載器。Java源代碼首先被jvm編譯成.class文件。類從被加載到虛擬機內存中開始,直到卸載出內存為止,它的整個生命周期包括了:加載、驗證、準備、解析、初始化、使用和卸載這7個階段。其中,驗證、準備和解析這三個部分統稱為連接(linking)。
加載:通過一個類的全限定名來獲取定義此類的二進制字節流,將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構,在內存中生成一個代表這個類的Class對象,作為方法去這個類的各種數據的訪問入口;
驗證:驗證是連接階段的第一步,這一階段的目的是確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬自身的安全;
準備:準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法去中進行分配。這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。
解析:解析階段是虛擬機將常量池內的符號(Class文件內的符號)引用替換為直接引用(指針)的過程。
初始化:初始化階段是類加載過程的最后一步,開始執行類中定義的Java程序代碼(字節碼)。
父類靜態域——》子類靜態域——》父類成員初始化——》父類構造塊——》父類構造方法——》子類成員初始化——》子類構造塊——》子類構造方法;
類加載檢查:虛擬機遇到一條 new 指令時,首先檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
分配內存:在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需的內存大小在類加載完成后便可確定,為對象分配空間的任務等同于把一塊確定大小的內存從 Java 堆中劃分出來。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
初始化零值:內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
設置對象頭:初始化零值完成之后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。
執行init方法:在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建才剛開始,<init> 方法還沒有執行,所有的字段都還為零。所以一般來說,執行 new 指令之后會接著執行 <init> 方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。
1.如果一個類加載器收到了類加載請求,它并不會自己先加載,而是把這個請求委托給父類的加載器去執行;
2.如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的引導類加載器;
3.如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成加載任務,子加載器才會嘗試自己去加載,這就是雙親委派機制;
避免類的重復加載,保護程序安全,防止核心API被隨意篡改。
JNDI(Java Naming and Directory Interface,Java命名和目錄接口)便是最典型的例子。JND需要調用由獨立廠商實現并部署在應用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,但啟動類加載器不可能“認識”這些代碼那該怎么辦?
為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的 setContextClassLoaser()方法進行設置。有了線程上下文類加載器,JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
例如JDBC在rt里面定義了這個SPI,那MySQL有MySQL的JDBC實現,Oracle有Oracle的JDBC實現,反正我java不管你內部如何實現的,反正你們都得統一按我這個來,這樣我們java開發者才能容易的調用數據庫操作。所以因為這樣那就不得不違反這個約束啊,Bootstrap ClassLoader就得委托子類來加載數據庫廠商們提供的具體實現。因為它的手只能摸到<JAVA_HOME>\lib中,其他的它無能為力,這就違反了自下而上的委托機制了。
不可以。因為在類加載中,會根據雙親委派機制去尋找當前java.lang.String是否已被加載。由于啟動類加載器已在啟動時候加載了所以不會再次加載,因此使用的String是已在java核心類庫加載過的String,而不是新定義的String。
Java源程序.java通過編譯器編譯成字節碼.class文件,也就是計算機可以識別的二進制文件。
根據 Java 虛擬機規范的規定,class 文件格式采用一種類似于 C 語言的偽結構來存儲數據,這種偽結構中只有兩種數據類型:無符號數和表。
無符號數屬于基礎數據類型,以 u1、u2、u4、u8 來分別代表 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照 UTF-8 編碼構成的字符串值。
表是由多個無符號數或者其他表作為數據項構成的復合數據結構,所有表都習慣性地以 _info 結尾。表用于描述有層次關系的復合結構的數據,整個 class 文件本質上就是一張表。
Java語言提供了對象終止(finalization)機制來允許開發人員提供對象被銷毀之前的自定義處理邏輯。當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象之前,總會先調用這個對象的finalize()方法,finalize()只會被調用一次。finalize() 方法允許在子類中被重寫,用于在對象被回收時進行資源釋放。通常在這個方法中進行一些資源釋放和清理的工作,比如關閉文件、套接字和數據庫連接等。
在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:
1)類的完整類名必須一致,包括包名。
2)加載這個類的ClassLoader(指ClassLoader實例對象)必須相同。
主動使用,分為七種情況:
1)創建類的實例
2)訪問某個類或接口的靜態變量,或者對該靜態變量賦值
3)調用類的靜態方法I
4)反射(比如:Class.forName(“com.atguigu.Test”))
5)初始化一個類的子類
6)Java虛擬機啟動時被標明為啟動類的類
7)JDK7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF getStatic、REF putStatic、REF invokeStatic句柄對應的類沒有初始化,則初始化
除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化。
淺拷貝:只是增加了一個指針指向已存在的內存地址,
深拷貝:是增加了一個指針并且申請了一個新的內存,使這個增加的指針指向這個新的內存,使用深拷貝的情況下,釋放內存的時候不會因為出現淺拷貝時釋放同一個內存的錯誤。
淺復制:僅僅是指向被復制的內存地址,如果原地址發生改變,那么淺復制出來的對象也會相應的改變。
深復制:在計算機中開辟一塊新的內存地址用于存放復制的對象。
java -XX:+TraceClassLoading 具體類
Java -verbose 具體類
JDK1.7相對于JDK1.6,主要的變化就是將永久代中的字符串常量池移到堆內存中,交由堆管理。我們知道堆是JVM內存管理的主要區域,那么將字符串常量池放到堆內存中更方便高效的對字符串常量進行管理和垃圾回收。
而JDK1.8相對于JDK1.7來說,主要區別有兩點,一是將虛擬機棧和本地方法棧合二為一了,二是移除永久代,增加了元數據區,元數據區使用本地內存,只受計算機內存大小的限制。而永久代使用的還是堆內存空間,受堆內存大小的限制。
記錄正在執行的虛擬機字節碼指令的地址。為了線程切換后能恢復到正確的位置,每個線程都需要一個獨立的程序計數器,各個線程之間互不影響,獨立存儲,這也就是所謂的“線程私有區域”。
描述方法執行的內存模型,每個方法在執行時都會創建一個棧幀,每個棧幀存放的是局部變量表,操作數棧,動態鏈接,方法出口等信息,方法被調用到執行完成對應的是一個棧幀從入棧到出棧的過程。是線程私有的。
為虛擬機使用到的Native方法服務。注在HotSpot虛擬機中直接就把本地方法棧和虛擬機棧合二為一了。
存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。在jdk1.8中已經去除了永久代,改用只受計算機本地內存大小限制的元空間來實現方法區,元空間參數(-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=1024M)。
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
1. 由于PermGen內存經常會溢出,引發惱人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的開發者希望這一塊內存可以更靈活地被管理,不要再經常出現這樣的 OOM。
2. 移除 PermGen 可以促進 HotSpot JVM 與 JRockit VM 的融合,因為 JRockit 沒有永久代。
i++:剛開始學java時候,i++先使用i,然后再自加1,為什么是這樣呢?
底層步驟:
1.從局部變量表取出 i 并壓入操作數棧。(入棧)
2.然后對局部變量表中的i自增1,將操作棧棧頂值取出使用。(自加1,出棧)
3.最后使用操作數棧的棧頂值更新局部變量表,如此線程從操作棧讀到的是自增之前的值。(更新)
++i:剛開始學java時候,++i先自加1然后再使用i,為什么是這樣呢?
----------------------------------------------------------
底層步驟:
1.先對局部變量表的 i 自增 1。(自加1)
2.然后取出并壓入操作數棧。(入棧)
3.再將操作棧棧頂值取出使用。(出棧)
4.最后使用棧頂值更新局部變量表,線程從操作棧讀到的是自增之后的值。(更新)
--------------------------------------------
之前之所以說 i++ 不是原子操作,即使使用 volatile 修飾也不是線程安全,就是因為可能 i 被從局部變量表取出,壓入操作數棧,操作數棧中自增,使用棧頂值更新局部變量表(寄存器更新寫入內存),其中分為 3 步,volatile 保證可見性,保證每次從局部變量表讀取的都是最新的值,但可能這 3 步可能被另一個線程的 3 步打斷,產生數據互相覆蓋問題,從而導致 i 的值比預期的小。
如果線程請求的棧深度大于虛擬機所允許的深度,則拋出StackOverflowError。
堆內存存儲對象實例。我們只要不斷地創建對象。并保證gc roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象。就會在對象數量到達最大。堆容量限制后,產生內存溢出異常。
public class Cat {
public static void main(String[] args) {
List list = new ArrayList();
while (true) {
list.add(new Cat());
}
}
}
1.棧是線程私有的,棧的生命周期和線程一樣,每個方法在執行的時候就會創建一個棧幀,它包含局部變量表、操作數棧、動態鏈接、方法出口等信息,局部變量表又包括基本數據類型和對象的引用;
2.當線程請求的棧深度超過了虛擬機允許的最大深度時,會拋出StackOverFlowError異常,方法遞歸調用肯可能會出現該問題;
3.調整參數-xss去調整jvm棧的大小;
在Java虛擬機中,對象是在Java堆中分配內存的,這是一個普遍的常識。但是,有一種特殊情況,那就是如果經過逃逸分析(Escape Analysis)后發現,一個對象并沒有逃逸出方法的話,那么就可能被優化成棧上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最常見的堆外存儲技術。
1)當一個對象在方法中被定義后,對象只在方法內部使用,則認為沒有發生逃逸。
2)當一個對象在方法中被定義后,它被外部方法所引用,則認為發生逃逸。例如作為調用參數傳遞到其他地方中。
運行時數據區 | 是否存在 | 是否存在 |
---|---|---|
程序計數器 | 否 | 否 |
虛擬機棧 | 是 | 否 |
本地方法棧 | 是 | 否 |
方法區 | 是(OOM) | 是 |
堆 | 是 | 是 |
Java GC(Garbage Collection)垃圾回收機制,是Java與C++/C的主要區別。JVM通過GC來回收堆和方法區中的內存,這個過程是自動執行的。因此作為Java開發者,不需要專門去編寫內存回收和垃圾清理代碼,也不需要處理內存泄露和溢出的問題。雖然java不需要開發人員顯示的分配和回收內存,這對開發人員確實降低了不少編程難度,但也可能帶來一些副作用:
1.有可能不知不覺浪費了很多內存;
2.JVM花費過多時間來進行內存回收;
3.內存泄露;
Java GC機制主要完成3件事:確定哪些內存需要回收;確定什么時候需要執行GC;如何執行GC。
1.引用計數法
所謂引用計數法就是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器為零時,說明此對象沒有被引用,也就是“死對象”,將會被垃圾回收.
引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象 A 引用對象 B,對象B 又引用者對象 A,那么此時 A,B 對象的引用計數器都不為零,也就造成無法完成垃圾回收,所以主流的虛擬機都沒有采用這種算法。
2.可達性算法(引用鏈法)
該算法的思想是:從一個被稱為 GC Roots的對象開始向下搜索,如果一個對象到 GCRoots 沒有任何引用鏈相連時,則說明此對象不可用。
在 java 中可以作為 GC Roots 的對象有以下幾種:
(1)虛擬機棧中引用的對象方法區類靜態屬性引用的對象方法區常量池引用的對象本地方法棧 JNI 引用的對象
雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象比不一定會被回收。當一個對象不可達 GC Root 時,這個對象并不會立馬被回收,而是出于一個死緩的階段,若要被真正的回收需要經歷兩次標記
(2)如果對象在可達性分析中沒有與 GC Root 的引用鏈,那么此時就會被第一次標記并且進行一次篩選,篩選的條件是是否有必要執行 finalize()方法。當對象沒有覆蓋 finalize()方法或者已被虛擬機調用過,那么就認為是沒必要的。
(3)如果該對象有必要執行 finalize()方法,那么這個對象將會放在一個稱為 F-Queue 的對隊列中,虛擬機會觸發一個 Finalize()線程去執行,此線程是低優先級的,并且虛擬機不會承諾一直等待它運行完,這是因為如果 finalize()執行緩慢或者發生了死鎖,那么就會造成 FQueue 隊列一直等待,造成了內存回收系統的崩潰。GC 對處于 F-Queue 中的對象進行第二次被標記,這時,該對象將被移除”即將回收”集合,等待回收。
GC按照回收區域又分為兩大種類型:部分收集和整堆收集。
部分收集(Partial GC):不是完整收集整個Java堆的垃圾收集。其中又分為:
1. 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
當年輕代空間不足時,就會觸發MinorGC,這里的年輕代滿指的是Eden代滿,Survivor滿不會引發GC。
2. 老年代收集(MajorGC/o1dGC):只是老年代的圾收集。
3. 混合收集(MixedGC):收集整個新生代以及部分老年代的垃圾收集。
整堆收集(FullGC):收集整個java堆和方法區的垃圾收集。
1.在初始階段,新創建的對象被分配到Eden區,S0和S1的兩塊空間都為空。
2.當Eden區滿了的時候,Minor GC 被觸發 。經過掃描與標記,不存活的對象被回收,存活的對象被復制到S0,并且存活的對象年齡都增大一歲。
3.當Eden區又滿的時候,Minor GC再次被觸發。此時Eden區 和 S0區存活的對象要復制到S1。需要注意的是,此時Eden區和S0區被清空,S0中的對象復制到S1后其年齡要加1。
4.當Eden區再次又滿的時候,MinorGC則重復上面過程,將Eden區 和 S1區存活的對象復制到S0。此時Eden區和S1區被清空,S0中的對象復制到S1后其年齡要加1。
5.經過幾次Minor GC之后,當存活對象的年齡達到一個閾值之后(-XX:MaxTenuringThreshold默認是15),就會被從年輕代Promotion到老年代。
6.新生代 GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC 非常頻繁,回收速度一般也比較快。
7.老年代 GC(Major GC/Full GC):指發生在老年代的 GC,出現了 Major GC 經常會伴隨至少一次的 Minor GC(并非絕對),Major GC 的速度一般會比 Minor GC 的慢 10 倍以上。
JDK1.2 以前,一個對象只有被引用和沒有被引用兩種狀態。后來,Java 對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4 種,這 4 種引用強度依次逐漸減弱。
一共有 4 種:標記-清除算法、復制算法、標記整理算法、分代收集算法;
最基礎的收集算法是“標記-清除”(Mark-Sweep)算法,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
● 效率問題,標記和清除兩個過程的效率都不高;
● 空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半。復制算法的執行過程如下圖:
一般虛擬機都采用這種算法來回收新生代,因為新生代中的對象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例來劃分內存空間,而是將內存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor 。當回收時,將 Eden 和 Survivor 中還存活著的對象一次性地復制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。HotSpot 虛擬機默認 Eden:Survivor = 8:1,也就是每次新生代中可用內存空間為整個新生代容量的 90%(其中一塊Survivor不可用),只有 10% 的內存會被“浪費”。當然如果另外一塊 Survivor 空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。
復制算法適合年輕代,不適合老年代。因為在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是復制算法需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都 100% 存活的極端情況。
根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
"分代收集"(Generational Collection)算法是根據對象存活周期的不同將內存劃分為幾塊并采用不同的垃圾收集算法。一般是把 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。
進行垃圾收集時,必須暫停其他所有工作線程,Sun將這種事情叫做"Stop The World"。
如果新生代的垃圾收集器為Serial和ParNew,并且設置了-XX:PretenureSizeThreshold參數,當對象大于這個參數值時,會被認為是大對象,直接進入老年代。
Young GC后,如果對象太大無法進入Survivor區,則會通過分配擔保機制進入老年代。
對象每在Survivor區中“熬過”一次Young GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲,可以通過-XX:MaxTenuringThreshold設置),就將會被晉升到老年代中。
如果在Survivor區中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
在HotSpot虛擬機中,Eden區和Survivor區的默認比例為8:1:1,即-XX:SurvivorRatio=8,其中Survivor分為From Survivor和ToSurvivor,因此Eden此時占新生代空間的80%。
Minor GC是當年輕代空間不足時,就會觸發MinorGC,這里的年輕代滿指的是Eden代滿,Survivor滿不會引發GC(每次Minor GC會清理年輕代的內存)。因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。這一定義既清晰又易于理解。Minor GC會引發STW,暫停其它用戶的線程,等垃圾回收結束,用戶線程才恢復運行
Major GC指發生在老年代的GC,對象從老年代消失時,我們說"Major Gc"或"Full GC"發生了。出現了MajorGc,經常會伴隨至少一次的Minor GC(但非絕對的,在Paralle1 Scavenge收集器的收集策略里就有直接進行MajorGC的策略選擇過程),也就是在老年代空間不足時,會先嘗試觸發MinorGc。如果之后空間還不足,則觸發Major GC,Major GC的速度一般會比MinorGc慢1e倍以上,STW的時間更長,如果Major GC后,內存還不足,就報OOM了;
Full GC是對年輕代和老年代都進行垃圾回收,Full GC 是開發或調優中盡量要避免的,這樣暫時時間會短一些。
Minor GC觸發條件: 當Eden區滿時,觸發Minor GC。
Full GC觸發條件:
(1)調用System.gc時,系統建議執行Full GC,但是不必然執行
(2)老年代空間不足
(3)方法區空間不足
(4)通過Minor GC后進入老年代的平均大小大于老年代的可用內存
(5)由Eden區、From Space區向To Sp3ace區復制時,對象大小大于To Space可存,則把該對象轉存到老年代,且老年代的可用內存小于該對象大小;
最基本的垃圾收集器,使用復制算法,單線程,雖然收集垃圾時需要暫停其他所有的工作線程,但簡單高效,是 java 虛擬機運行在 Client 模式下默認的新生代垃圾收集器。
在HotSpot虛擬機中,使用-XX:+UseSerialGC參數可以指定年輕代和老年代都使用串行收集器。等價于新生代用Serial GC,且老年代用Serial Old GC。
是 Serial 收集器的多線程版本 ,除了多線程進行GC外,其他與Serial一樣,默認開啟和 CPU 數目相同的線程數 。是很多 java虛擬機運行在 Server 模式下新生代的默認垃圾收集器。
1)可以通過選項"-XX:+UseParNewGC"手動指定使用ParNew收集器執行內存回收任務。它表示年輕代使用并行收集器,不影響老年代
2)這里的多線程指的是垃圾收集時,多線程并行,并不是垃圾收集與程序運行并行
3)收集垃圾時,也需要暫停其他所有工作線程,然后多線程收集垃圾。
4)單CPU環境下,因為線程切換,性能較差。
關注程序的吞吐量,即吞吐量優先。主要適用于在后臺運算而不需要太多交互的任務。 自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個重要區別。
1)吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間) ; 吞吐量優先,意味著在單位時間內,STW的時間最短;與之相對應的 低延遲 就是暫停時間優先,盡可能讓單次STW時間最短;這兩個無法同時實現。
2)收集垃圾時,也需要暫停其他所有工作線程,然后多線程收集垃圾。
3)參數配置
-XX:+UseParallelGC 手動指定年輕代使用Parallel并行收集器執行內存回收任務。
-XX:+UseParallelOldGC 手動指定老年代都是使用并行回收收集器。
-XX:ParallelGCThreads 設置年輕代并行收集器的線程數。一般地,最好與CPU數量相等,以避免過多的線程數影響垃圾收集性能。
-XX:MaxGCPauseMillis 設置垃圾收集器最大停頓時間(即STw的時間),單位是毫秒。 為了盡可能地把停頓時間控制在MaxGCPauseMills以內,收集器在工作時會調整Java堆大小或者其他一些參數。對于用戶來講,停頓時間越短體驗越好。但是在服務器端,我們注重高并發,整體的吞吐量。所以服務器端適合Parallel進行控制。該參數使用需謹慎。
-XX:GCTimeRatio 垃圾收集時間占總時間的比例(=1/(N+1))。用于衡量吞吐量的大小。 取值范圍(0, 100)。默認值99,也就是垃圾回收時間不超過1%。與前一個-XX:MaxGCPauseMillis參數有一定矛盾性。暫停時間越長,Radio參數就容易超過設定的比例。
-XX:+UseAdaptivesizePolicy 設置Parallel Scavenge收集器具有自適應調節策略 。在這種模式下,年輕代的大小、Eden和Survivor的比例、晉升老年代的對象年齡等參數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。在手動調優比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMills,讓虛擬機自己完成調優工作。
1)是Serial的老年代版本,收集垃圾時也需要暫停其他所有的工作線程。
2)是Client模式下默認的老年代垃圾收集器
3)Server模式下,搭配新生代的Parallel Scavenge 收集器使用(在 JDK1.5 之前版本中)。同時也作為老年代中使用 CMS 收集器的后備垃圾收集方案(當CMS發生Concurrent Mode Failure)。
1)Parallel Scavenge的老年代版本
2)吞吐量優先,意味著在單位時間內,STW的時間最短;與之相對應的 低延遲 就是暫停時間優先,盡可能讓單次STW時間最短;這兩個無法同時實現。
3)若相同對于吞吐量要求較高,可以Parallel Scavenge搭配Parallel Old使用。
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“標記—清除”算法實現的,它的運作過程可以分為6個步驟,包括:初始標記、并發標記、預處理、重新標記、并發清除、重置。
CMS是一款優秀的收集器,它的主要優點在名字上已經體現出來了:并發收集、低停頓,但是CMS還遠達不到完美的程度,它有以下3個明顯的缺點:
(1)CMS收集器對CPU資源非常敏感。
(2)CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。
(3)CMS是一款基于“標記—清除”算法實現的收集器,這意味著收集結束時會有大量空間碎片產生。
G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一。G1是一款面向服務端應用的垃圾收集器。與其他GC收集器相比,G1具備如下特點:并行與并發、分代收集、空間整合、可預測的停頓。
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域,雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
Mixed GC是G1垃圾收集器特有的收集方式,Mixed GC大致可劃分為全局并發標記(global concurrent marking)和拷貝存活對象(evacuation)兩個大部分:
global concurrent marking是基于SATB形式的并發標記,包括以下4個階段:初始標記(Initial Marking)、并發標記(Concurrent Marking)、最終標記(Final Marking)、清理(Clean Up)。Evacuation階段是全暫停的。它負責把一部分region里的活對象拷貝到空region里去,然后回收原本的region的空間。
垃圾收集器 | 分類 | 作用位置 | 使用算法 | 特點 | 適用場景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 復制算法 | 響應速度優先 | 適用于單CPU環境下的client模式 |
ParNew | 并行 | 新生代 | 復制算法 | 響應速度優先 | 多CPU環境Server模式下與CMS配合使用 |
Parallel | 并行 | 新生代 | 復制算法 | 吞吐量優先 | 適用于后臺運算而不需要太多交互的場景 |
Serial Old | 串行 | 老年代 | 標記-整理(壓縮)算法 | 響應速度優先 | 適用于單CPU環境下的Client模式 |
Paraller Old | 并行 | 老年代 | 標記-整理(壓縮)算法 | 標記-整理(壓縮)算法 | 適用于后臺運算而不需要太多交互的場景 |
CMS | 并發 | 老年代 | 標記-清除算法 | 響應速度優先 | 適用于互聯網或B/S業務 |
G1 | 并發、并行 | 新生代、老年代 | 標記-整理(壓縮)算法 | 響應速度優先 | 響應速度優先 |
1)多數的Java應用不需要在服務器上進行GC優化;
2)多數導致GC問題的Java應用,都不是因為我們參數設置錯誤,而是代碼問題;
3)在應用上線前,先考慮將JVM參數設置到最優;
4)減少對象創建的數量;
5)減少全局變量和大對象;
6)GC優化是最后不得已才使用的手段,在實際應用中,分析GC情況優化代碼比優化GC參數要多得多;
通過看監控中的jvm是否有fgc,頻繁fgc才需要優化(頻繁fgc需要抓緊改配置)
1)JDK的命令行工具
Sun JDK監控和故障處理命令有jps、jstat、jmap、jhat、jstack、jinfo
jps(虛擬機進程狀況工具):顯示指定系統內所有的HotSpot虛擬機進程。 jstat(虛擬機統計信息監視工具):用于監視虛擬機運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據。 jinfo(Java配置信息工具):jinfo的作用是實時地查看和調整虛擬機各項參數。
jmap(Java內存映像工具):dump堆到文件,可用于對文件的分析。
jhat(虛擬機堆轉儲快照分析工具):jhat命令與jmap搭配使用,來分析jmap生成的堆 轉儲快照。jhat內置了一個微型的HTTP/HTML服務器,生成dump文件的分析結果后,可以在瀏覽器中查看。
jstack(Java堆棧跟蹤工具):jstack命令用于生成虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧 的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循 環、請求外部資源導致的長時間等待等都是導致線程長時間停頓的常見原因。線程出現停頓 的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在后臺做些 什么事情,或者等待著什么資源。
2)JConsole
Jconsole(Java Monitoring and Management Console)是從java5開始,在JDK中自帶的java監控和管理控制臺,用于對JVM中內存,線程和類等的監控,是一個基于JMX(java management extensions)的GUI性能監測工具。jconsole使用jvm的擴展機制獲取并展示虛擬機中運行的應用程序的性能和資源消耗等信息。
概覽:包括堆內存使用情況、線程、類、CPU使用情況四項信息的曲線圖。
3)VisualVM
VisualVM(All-in-One Java Troubleshooting Tool)是功能最強大的運行監視和故障處理程序之一,曾經在很長一段時間內是Oracle官方主力發展的虛擬機故障處理工具。
相比一些第三方工具,VisualVM有一個很大的優點:不需要被監視的程序基于特殊Agent去運行,因此它的通用性很強,對應用程序實際性能的影響也較小,使得它可以直接應用在生產環境中。
Visual GC 是常常使用的一個功能,需要通過插件按照,可以明顯的看到年輕代、老年代的內存變化,以及gc頻率、gc的時間等!
觸發 java.lang.OutOfMemoryError:最常見的原因就是應用程序需要的堆空間需要的是大的,但是 JVM 提供的卻是小的,從而導致內存溢出。這個解決方法就是提供大的堆空間即可。
除此之外還有復雜的原因:內存泄露。特定的編程錯誤會導致你的應用程序不停的消耗更多的內存,每次使用有內存泄漏風險的功能就會留下一些不能被回收的對象到堆空間中,隨著時間的推移,泄漏的對象會消耗所有的堆空間,最終觸發java.lang.OutOfMemoryError: Java heap space 錯誤。
1.確保有足夠的堆空間來正常運行你的應用程序,在 JVM 的啟動配置中增加如下配置:-Xmx1024m。
2.流量/數據量峰值:應用程序在設計之初均有用戶量和數據量的限制,某一時刻,當用戶數量或數據量突然達到一個 峰 值 , 并 且 這 個 峰 值 已 經 超 過 了 設 計 之 初 預 期 的 閾 值 , 那 么 以 前 正 常 的 功 能 將 會 停 止 , 并 觸 發java.lang.OutOfMemoryError
3.Java heap space 異常解決方案,如果你的應用程序確實內存不足,增加堆內存會解決 GC overhead limit 問題,就如下面這樣,給你的應用程序 1G 的堆內存:java -Xmx1024m com.yourcompany.YourClass。