文章對 JVM 內存區域分佈、JVM 內存溢出分析、JVM 垃圾回收算法/垃圾收集器、JVM 性能調優工具及技巧、類加載等高頻問點作了解析。java
用XMind畫了一張導圖(源文件對部分節點有詳細備註和參考資料,歡迎關注個人公衆號:以Java架構贏天下 後臺發送【JVM】拿下載連接,後面完善增長了會及時更新):程序員
以下圖所示:面試
黃色部分爲線程共有,藍色部分爲線程私有。算法
用於存儲虛擬機加載的類信息,常量,靜態變量等數據。編程
存放對象實例,全部的對象和數組都要在堆上分配。 是 JVM 所管理的內存中最大的一塊區域。後端
Java 方法執行的內存模型:存儲局部變量表,操做數棧,動態連接,方法出口等信息。生命週期與線程相同。數組
做用與虛擬機棧相似,不一樣點本地方法棧爲 native 方法執行服務,虛擬機棧爲虛擬機執行的 Java 方法服務。緩存
當前線程所執行的行號指示器。是 JVM 內存區域最小的一塊區域。執行字節碼工做時就是利用程序計數器來選取下一條須要執行的字節碼指令。安全
寄存器:咱們沒法控制。
靜態域:static定義的靜態成員。
常量池:編譯時被肯定並保存在 .class 文件中的(final)常量值和一些文本修飾的符號引用(類和接口的全限定名,字段的名稱和描述符,方法和名稱和描述符)。
非 RAM 存儲:硬盤等永久存儲空間。
堆內存:new 建立的對象和數組,由 Java 虛擬機自動垃圾回收器管理,存取速度慢。
棧內存:基本類型的變量和對象的引用變量(堆內存空間的訪問地址),速度快,能夠共享,可是大小與生存期必須肯定,缺少靈活性。
3.Java 堆的結構是什麼樣子的?什麼是堆中的永久代(Perm Gen space)?
JVM 的堆是運行時數據區,全部類的實例和數組都是在堆上分配內存。它在 JVM 啓動的時候被建立。對象所佔的堆內存是由自動內存管理系統也就是垃圾收集器回收。
堆內存是由存活和死亡的對象組成的。存活的對象是應用能夠訪問的,不會被垃圾回收。死亡的對象是應用不可訪問尚且尚未被垃圾收集器回收掉的對象。一直到垃圾收集器把這些 對象回收掉以前,他們會一直佔據堆內存空間。多線程
import java.io.IOException;
public class GarbageTest {
/** * @param args * @throws IOException */
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
try {
gcTest();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("has exited gcTest!");
System.in.read();
System.in.read();
System.out.println("out begin gc!");
for(int i=0;i<100;i++)
{
System.gc();
System.in.read();
System.in.read();
}
}
private static void gcTest() throws IOException {
System.in.read();
System.in.read();
Person p1 = new Person();
System.in.read();
System.in.read();
Person p2 = new Person();
p1.setMate(p2);
p2.setMate(p1);
System.out.println("before exit gctest!");
System.in.read();
System.in.read();
System.gc();
System.out.println("exit gctest!");
}
private static class Person {
byte[] data = new byte[20000000];
Person mate = null;
public void setMate(Person other) {
mate = other;
}
}
}
複製代碼
Java 中的內存泄露的狀況:長生命週期的對象持有短生命週期對象的引用就極可能發生內存泄露,儘管短生命週期對象已經再也不須要,可是由於長生命週期對象持有它的引用而致使不能被回收,這就是 Java 中內存泄露的發生場景,通俗地說,就是程序員可能建立了一個對象,之後一直再也不使用這個對象,這個對象卻一直被引用,即這個對象無用可是卻沒法被垃圾回收器回收的,這就是 java 中可能出現內存泄露的狀況,例如,緩存系統,咱們加載了一個對象放在緩存中 (例如放在一個全局 map 對象中),而後一直再也不使用它,這個對象一直被緩存引用,但卻再也不被使用。
檢查 Java 中的內存泄露,必定要讓程序將各類分支狀況都完整執行到程序結束,而後看某個對象是否被使用過,若是沒有,則才能斷定這個對象屬於內存泄露。
若是一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即便那個外部類實例對象再也不被使用,但因爲內部類持久外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會形成內存泄露。
下面內容來自於網上(主要特色就是清空堆棧中的某個元素,並非完全把它從數組中拿掉,而是把存儲的總數減小,本人寫得能夠比這個好,在拿掉某個元素時,順便也讓它從數組中消失,將那個元素所在的位置的值設置爲 null 便可):
我實在想不到比那個堆棧更經典的例子了,以至於我還要引用別人的例子,下面的例子不是我想到的,是書上看到的,固然若是沒有在書上看到,可能過一段時間我本身也想的到,但是那時我說是我本身想到的也沒有人相信的。
public class Stack {
private Object[] elements=new Object[10];
private int size = 0;
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if( size == 0) throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity(){
if(elements.length == size){
Object[] oldElements = elements;
elements = new Object[2 * elements.length+1];
System.arraycopy(oldElements,0, elements, 0, size);
}
}
}
複製代碼
上面的原理應該很簡單,假如堆棧加了 10 個元素,而後所有彈出來,雖然堆棧是空的,沒有咱們要的東西,可是這是個對象是沒法回收的,這個才符合了內存泄露的兩個條件:無用,沒法回收。可是就是存在這樣的東西也不必定會致使什麼樣的後果,若是這個堆棧用的比較少,也就浪費了幾個K內存而已,反正咱們的內存都上 G 了,哪裏會有什麼影響,再說這個東西很快就會被回收的,有什麼關係。下面看兩個例子。
public class Bad{
public static Stack s=Stack();
static{
s.push(new Object());
s.pop(); //這裏有一個對象發生內存泄露
s.push(new Object()); //上面的對象能夠被回收了,等因而自愈了
}
}
複製代碼
由於是 static,就一直存在到程序退出,可是咱們也能夠看到它有自愈功能,就是說若是你的 Stack 最多有 100 個對象,那麼最多也就只有 100 個對象沒法被回收其實這個應該很容易理解,Stack 內部持有 100 個引用,最壞的狀況就是他們都是無用的,由於咱們一旦放新的進取,之前的引用天然消失!
內存泄露的另一種狀況:當一個對象被存儲進 HashSet 集合中之後,就不能修改這個對象中的那些參與計算哈希值的字段了,不然,對象修改後的哈希值與最初存儲進 HashSet 集合中時的哈希值就不一樣了,在這種狀況下,即便在 contains 方法使用該對象的當前引用做爲的參數去 HashSet 集合中檢索對象,也將返回找不到對象的結果,這也會致使沒法從 HashSet 集合中單獨刪除當前對象,形成內存泄露。
GC 是垃圾收集的意思(GabageCollection),內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會致使程序或系統的不穩定甚至崩潰,Java 提供的 GC 功能能夠自動監測對象是否超過做用域從而達到自動回收內存的目的,Java 語言沒有提供釋放已分配內存的顯示操做方法。
在 Java 中,程序員是不須要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在 JVM 中,有一個垃圾回收線程,它是低優先級的,在正常狀況下是不會執行的,只有在虛擬機空閒或者當前堆內存不足時,纔會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。
判斷一個對象是否存活有兩種方法:
- 引用計數法
所謂引用計數法就是給每個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器爲零時,說明此對象沒有被引用,也就是「死對象」,將會被垃圾回收.
引用計數法有一個缺陷就是沒法解決循環引用問題,也就是說當對象 A 引用對象 B,對象 B 又引用者對象 A,那麼此時 A、B 對象的引用計數器都不爲零,也就形成沒法完成垃圾回收,因此主流的虛擬機都沒有采用這種算法。- 可達性算法(引用鏈法)
該算法的思想是:從一個被稱爲 GC Roots 的對象開始向下搜索,若是一個對象到 GC Roots 沒有任何引用鏈相連時,則說明此對象不可用。
在 Java 中能夠做爲 GC Roots 的對象有如下幾種:
1.虛擬機棧中引用的對象
2.方法區類靜態屬性引用的對象
3.方法區常量池引用的對象
4.本地方法棧JNI引用的對象
雖然這些算法能夠斷定一個對象是否能被回收,可是當知足上述條件時,一個對象比不必定會被回收。當一個對象不可達 GC Root 時,這個對象並不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收須要經歷兩次標記.
若是對象在可達性分析中沒有與 GC Root 的引用鏈,那麼此時就會被第一次標記而且進行一次篩選,篩選的條件是是否有必要執行 finalize() 方法。當對象沒有覆蓋 finalize() 方法或者已被虛擬機調用過,那麼就認爲是不必的。 若是該對象有必要執行 finalize() 方法,那麼這個對象將會放在一個稱爲 F-Queue 的對隊列中,虛擬機會觸發一個 Finalize() 線程去執行,此線程是低優先級的,而且虛擬機不會承諾一直等待它運行完,這是由於若是 finalize() 執行緩慢或者發生了死鎖,那麼就會形成 F-Queue 隊列一直等待,形成了內存回收系統的崩潰。GC 對處於 F-Queue 中的對象進行第二次被標記,這時,該對象將被移除」 即將回收」 集合,等待回收。
Java 語言中一個顯著的特色就是引入了垃圾回收機制,使 C++ 程序員最頭疼的內存管理的問題迎刃而解,它使得 Java 程序員在編寫程序的時候再也不須要考慮內存管理。因爲有個垃圾回收機制,Java 中的對象再也不有「做用域」的概念,只有對象的引用纔有"做用域"。垃圾回收能夠有效的防止內存泄露,有效的使用可使用的內存。垃圾回收器一般是做爲一個單獨的低級別的線程運行,不可預知的狀況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清楚和回收,程序員不能實時的調用垃圾回收器對某個對象或全部對象進行垃圾回收。
回收機制有分代複製垃圾回收和標記垃圾回收,增量垃圾回收。
對於 GC 來講,當程序員建立對象時,GC 就開始監控這個對象的地址、大小以及使用狀況。一般,GC 採用有向圖的方式記錄和管理堆(heap)中的全部對象。經過這種方式肯定哪些對象是」可達的」,哪些對象是」不可達的」。當 GC 肯定一些對象爲「不可達」時,GC 就有責任回收這些內存空間。能夠。程序員能夠手動執行 System.gc(),通知 GC 運行,可是 Java 語言規範並不保證 GC 必定會執行。
DGC 叫作分佈式垃圾回收。RMI 使用 DGC 來作自動垃圾回收。由於 RMI 包含了跨虛擬機的遠程對象的引用,垃圾回收是很困難的。DGC 使用引用計數算法來給遠程對象提供自動內存管理。
吞吐量收集器使用並行版本的新生代垃圾收集器,它用於中等規模和大規模數據的應用程序。 而串行收集器對大多數的小應用(在現代處理器上須要大概 100M 左右的內存)就足夠了。
當對象對當前使用這個對象的應用程序變得不可觸及的時候,這個對象就能夠被回收了。
新生代垃圾收集器
Serial 收集器
特色: Serial 收集器只能使用一條線程進行垃圾收集工做,而且在進行垃圾收集的時候,全部的工做線程都須要中止工做,等待垃圾收集線程完成之後,其餘線程才能夠繼續工做。
使用算法:複製算法
ParNew 收集器
特色: ParNew 垃圾收集器是Serial收集器的多線程版本。爲了利用 CPU 多核多線程的優點,ParNew 收集器能夠運行多個收集線程來進行垃圾收集工做。這樣能夠提升垃圾收集過程的效率。
使用算法:複製算法
Parallel Scavenge 收集器
特色: Parallel Scavenge 收集器是一款多線程的垃圾收集器,可是它又和 ParNew 有很大的不一樣點。
Parallel Scavenge 收集器和其餘收集器的關注點不一樣。其餘收集器,好比 ParNew 和 CMS 這些收集器,它們主要關注的是如何縮短垃圾收集的時間。而 Parallel Scavenge 收集器關注的是如何控制系統運行的吞吐量。這裏說的吞吐量,指的是 CPU 用於運行應用程序的時間和 CPU 總時間的佔比,吞吐量 = 代碼運行時間 / (代碼運行時間 + 垃圾收集時間)。若是虛擬機運行的總的 CPU 時間是 100 分鐘,而用於執行垃圾收集的時間爲 1 分鐘,那麼吞吐量就是 99%。
使用算法:複製算法
老年代垃圾收集器
Serial Old 收集器
特色: Serial Old 收集器是 Serial 收集器的老年代版本。這款收集器主要用於客戶端應用程序中做爲老年代的垃圾收集器,也能夠做爲服務端應用程序的垃圾收集器。
使用算法:標記-整理
Parallel Old 收集器
特色: Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本這個收集器是在 JDK1.6 版本中出現的,因此在 JDK1.6 以前,新生代的 Parallel Scavenge 只能和 Serial Old 這款單線程的老年代收集器配合使用。Parallel Old 垃圾收集器和 Parallel Scavenge 收集器同樣,也是一款關注吞吐量的垃圾收集器,和 Parallel Scavenge 收集器一塊兒配合,能夠實現對 Java 堆內存的吞吐量優先的垃圾收集策略。
使用算法:標記-整理
CMS 收集器
特色: CMS 收集器是目前老年代收集器中比較優秀的垃圾收集器。CMS 是 Concurrent Mark Sweep,從名字能夠看出,這是一款使用"標記-清除"算法的併發收集器。
CMS 垃圾收集器是一款以獲取最短停頓時間爲目標的收集器。以下圖所示:
從圖中能夠看出,CMS 收集器的工做過程能夠分爲 4 個階段:
使用算法:複製+標記清除
其餘
G1 垃圾收集器
特色: 主要步驟:初始標記,併發標記,從新標記,複製清除。
使用算法:複製 + 標記整理
標記 - 清除:這是垃圾收集算法中最基礎的,根據名字就能夠知道,它的思想就是標記哪些要被回收的對象,而後統一回收。這種方法很簡單,可是會有兩個主要問題:
複製算法:爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,而後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,而後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。可是這種方式,內存的代價過高,每次基本上都要浪費通常的內存。
因而將該算法進行了改進,內存區域再也不是按照 1:1 去劃分,而是將內存劃分爲 8:1:1 三部分,較大那分內存交 Eden 區,其他是兩塊較小的內存區叫 Survior 區。每次都會優先使用 Eden 區,若 Eden 區滿,就將對象複製到第二塊內存區上,而後清除 Eden 區,若是此時存活的對象太多,以致於 Survivor 不夠時,會將這些對象經過分配擔保機制複製到老年代中。(java 堆又分爲新生代和老年代)
標記 - 整理:該算法主要是爲了解決標記 - 清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了複製算法的效率問題。它的不一樣之處就是在清除對象的時候現將可回收對象移動到一端,而後清除掉端邊界之外的對象,這樣就不會產生內存碎片了。
分代收集:如今的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生代和老年代。在新生代中,因爲對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保。
實現經過類的權限定名獲取該類的二進制字節流的代碼塊叫作類加載器。
主要有一下四種類加載器:
1.啓動類加載器(Bootstrap ClassLoader)用來加載 Java 核心類庫,沒法被 Java 程序直接引用。
2.擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
3.系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。能夠經過 ClassLoader.getSystemClassLoader() 來獲取它。
4.用戶自定義類加載器,經過繼承 java.lang.ClassLoader 類的方式實現。
基本定義: 雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器沒有找到所需的類時,子加載器纔會嘗試去加載該類。
雙親委派機制:
當 AppClassLoader 加載一個 class 時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器 ExtClassLoader 去完成。
當 ExtClassLoader 加載一個 class 時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給 BootStrapClassLoader 去完成。
若是 BootStrapClassLoader 加載失敗,會使用 ExtClassLoader 來嘗試加載;
若 ExtClassLoader 也加載失敗,則會使用 AppClassLoader 來加載,若是 AppClassLoader 也加載失敗,則會報出異常 ClassNotFoundException。
以下圖所示:
雙親委派做用:
Class 文件是一組以 8 位字節爲基礎單位的二進制流。各個數據項嚴格按順序排列。
Class 文件格式採用一種相似於 C 語言結構體的僞結構來存儲數據。這樣的僞結構僅僅有兩種數據類型:無符號數和表。
無符號數:是基本數據類型。以 u一、u二、u四、u8 分別表明 1 個字節、2 個字節、4 個字節、8 個字節的無符號數,可以用來描寫敘述數字、索引引用、數量值或者依照 UTF-8 編碼構成的字符串值。
表:由多個無符號數或者其它表做爲數據項構成的複合數據類型。所有表都習慣性地以 _info 結尾。
除了數據運行區,其餘區域均有可能形成 OOM 的狀況。
堆溢出:java.lang.OutOfMemoryError: Java heap space
棧溢出:java.lang.StackOverflowError
永久代溢出:java.lang.OutOfMemoryError: PermGen space
數據區設置
Xms:初始堆大小
Xmx:最大堆大小
Xss:Java 每一個線程的Stack大小
XX:NewSize=n:設置年輕代大小
XX:NewRatio=n:設置年輕代和年老代的比值。如:爲 3,表示年輕代與年老代比值爲 1:3,年輕代佔整個年輕代年老代和的 1/4。
XX:SurvivorRatio=n:年輕代中 Eden 區與兩個 Survivor 區的比值。注意 Survivor 區有兩個。如:3,表示 Eden:Survivor=3:2,一個 Survivor 區佔整個年輕代的 1/5。
XX:MaxPermSize=n:設置持久代大小。
收集器設置
XX:+UseSerialGC:設置串行收集器
XX:+UseParallelGC::設置並行收集器
XX:+UseParalledlOldGC:設置並行年老代收集器
XX:+UseConcMarkSweepGC:設置併發收集器
GC日誌打印設置
XX:+PrintGC:打印 GC 的簡要信息
XX:+PrintGCDetails:打印 GC 詳細信息
XX:+PrintGCTimeStamps:輸出 GC 的時間戳
jps:用來顯示本地的 Java 進程,能夠查看本地運行着幾個 Java 程序,並顯示他們的進程號。 命令格式:jps
jinfo:運行環境參數:Java System 屬性和 JVM 命令行參數,Java class path 等信息。 命令格式:jinfo 進程 pid
jstat:監視虛擬機各類運行狀態信息的命令行工具。 命令格式:jstat -gc 123 250 20
jstack:能夠觀察到 JVM 中當前全部線程的運行狀況和線程當前狀態。 命令格式:jstack 進程 pid
jmap:觀察運行中的 JVM 物理內存的佔用狀況(如:產生哪些對象,及其數量)。 命令格式:jmap [option] pid
2020年面試必備的Java後端進階面試題總結了一份將近500頁的pdf文檔,歡迎關注個人公衆號:以Java架構贏天下,回覆【2020】領取這些整理的資料!
喜歡文章記得關注我點個贊喲,感謝支持!