1. 程式人生 > 其它 >被面試官問臭了的jvm效能優化你還不知道?建議收藏!

被面試官問臭了的jvm效能優化你還不知道?建議收藏!

技術標籤:jvmjava程式語言

一 、JVM效能優化專題

1、Java類載入過程

Java 類載入需要經歷一下7 個過程:

1.1 . 載入

載入是類載入的第一個過程,在這個階段,將完成一下三件事情:

  • 通過一個類的全限定名獲取該類的二進位制流。
  • 將該二進位制流中的靜態儲存結構轉化為方法去執行時資料結 構。
  • 在記憶體中生成該類的Class物件,作為該類的資料訪問入口。

1.2 . 驗證

驗證的目的是為了確保Class,檔案的位元組流中的資訊不回危害到虛擬機器.在該階段主要完成以下四鍾驗證:

  • 檔案格式驗證:驗證位元組流是否符合檔案的規範,如主次版本號 是否在當前虛擬機器範圍內,常量池中的常量是否有不被支援的型別.
  • 元資料驗證:對位元組碼描述的資訊進行語義分析,如這個類是否有父類, 是否集成了不被繼承的類等。
  • 位元組碼驗證:是整個驗證過程中最複雜的一個階段,通過驗證資料流和 控制流的分析,確定程式語義是否正確,主要針對方法體的驗證。如:方 法中的型別轉換是否正確,跳轉指令是否正確等。
  • 符號引用驗證:這個動作在後面的解析過程中發生,主要是為了確保解析動作能正確執行。

1.3 . 準備

準備階段是為類的靜態變數分配記憶體並將其初始化為預設值,這些記憶體都將在方法區中進行分配。準備階段不分配類中的例項變數的記憶體,例項變數將會在物件例項化時隨著物件一起分配在Java 堆中。

 public static int value=
123;//在準備階段value初始值為0 。在初始化階段才會變為123 。

1.4 . 解析

該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初 始化動作完成之前,也有可能在初始化之後。

1.5 . 初始化

初始化時類載入的最後一步,前面的類載入過程,除了在載入階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中的定義的java程式程式碼。

1.6. 使用

1.7. 解除安裝

2、java記憶體分配

  • 暫存器:我們無法控制。
  • 靜態域:static定義的靜態成員。
  • 常量池:編譯時被確定並儲存在.class檔案中的(final)常量值和一些文字修飾的符號引用(類和介面的全限定名,欄位的名稱和描述符, 方法和名稱和描述符)。
  • 非RAM儲存:硬碟等永久儲存空間。
  • 堆記憶體:new 建立的物件和陣列,由Java 虛擬機器自動垃圾回收器管理, 存取速度慢。
  • 棧記憶體:基本型別的變數和物件的引用變數(堆記憶體空間的訪問地址),速度快,可以共享,但是大小與生存期必須確定,缺乏靈活性。

2.1.Java 堆的結構是什麼樣子的2什麼是堆中的永久代?

JVM的堆是執行時資料區,所有類的例項和陣列都是在堆上分配記憶體。它
在JVM啟動的時候被建立。物件所佔的堆記憶體是由自動記憶體管理系統也就
是垃圾收集器回收。
堆記憶體是由存活和死亡的物件組成的。存活的物件是應用可以訪問的,不會被垃圾回收。死亡的物件是應用不可訪問尚且還沒有被垃圾收集器回收 掉的物件。一直到垃圾收集器把這些物件回收掉之前,他們會一直佔據堆記憶體空間。

3、描述一下JVM載入Class檔案的原理機制2

Java 語言是一種具有動態性的解釋型語言,類(Class)只有被載入到JVM 後才能執行。當執行指定程式時,JVM 會將編譯生成的 .class
檔案按照需求和一定的規則載入到記憶體中,並組織成為一個完整的Java 應用程式。這個載入過程是由類載入器完成,具體來說,就是由 ClassLoader 和它的子類來實現的。類載入器本身也是一個類,其實質是把類檔案從硬碟讀取到記憶體中。
類的載入方式分為隱式載入和顯示載入。隱式載入指的是程式在使用new 等方式建立物件時,會隱式地呼叫類的載入器把對應的類載入到JVM 中。顯示載入指的是通過直接呼叫class.forName() 方法來把所需的類載入到JVM 中。

任何一個工程專案都是由許多類組成的,當程式啟動時,只把需要的類加 載到JVM 中,其他類只有被使用到的時候才會被載入,採用這種方法一方面可以加快載入速度,另一方面可以節約程式執行時對記憶體的開銷。此外,在Java 語言中,每個類或介面都對應一個 檔案.class,這些檔案可以被看成是一個個可以被動態載入的單元,因此當只有部分類被修改 時,只需要重新編譯變化的類即可,而不需要重新編譯所有檔案,因此加 快了編譯速度。
在Java 語言中,類的載入是動態的,它並不會一次性將所有類全部載入後再執行,而是保證程式執行的基礎類(例如基類)完全載入到JVM 中,至於其他類,則在需要的時候才載入。
類載入的主要步驟:

  • 裝載。根據查詢路徑找到相應的class 檔案,然後匯入。
  • 連結。連結又可分為3 個小步:
  • 檢查,檢查待載入的class 檔案的正確性。
  • 準備,給類中的靜態變數分配儲存空間。
  • 解析,將符號引用轉換為直接引用(這一步可選)
  • 初始化。對靜態變數和靜態程式碼塊執行初始化工作。

4、GC 是什麼? 為什麼要有 GC?

GC 是垃圾收集的意思(GabageCollection),記憶體處理是程式設計 人員容易出現問題的地方,忘記或者錯誤的記憶體回收會導致程式或 系統的不穩定甚至崩潰,Java 提供的 GC 功能可以自動監測物件 是否超過作用域從而達到自動回收記憶體的目的,Java 語言沒有提 供釋放已分配記憶體的顯示操作方法。

5、簡述 Java 垃圾回收機制)

在 Java 中,程式設計師是不需要顯示的去釋放一個物件的記憶體的,而 是由虛擬機器自行執行。在 JVM 中,有一個垃圾回收執行緒,它是低 優先順序的,在正常情況下是不會執行的,只有在虛擬機器空閒或者當 前堆記憶體不足時,才會觸發執行,掃面那些沒有被任何引用的物件, 並將它們新增到要回收的集合中,進行回收。

6、 如何判斷一個物件是否存活?(或者 GC 物件的判定方法)

判斷一個物件是否存活有兩種方法:

6.1 . 引用計數法

所謂引用計數法就是給每一個物件設定一個引用計數器,每當有一 個地方引用這個物件時,就將計數器加一,引用失效時,計數器就 減一。當一個物件的引用計數器為零時,說明此物件沒有被引用, 也就是“死物件”,將會被垃圾回收.
引用計數法有一個缺陷就是無法解決迴圈引用問題,也就是說當對 象 A 引用物件 B,物件 B 又引用者物件 A,那麼此時 A、B 對 象的引用計數器都不為零,也就造成無法完成垃圾回收,所以主流 的虛擬機器都沒有采用這種演算法。

6.2 . 可達性演算法(引用鏈法)

該演算法的思想是:從一個被稱為 GC Roots 的物件開始向下搜尋, 如果一個物件到 GC Roots 沒有任何引用鏈相連時,則說明此對 象不可用。在 Java 中可以作為 GC Roots 的物件有以下幾種:

  • 虛擬機器棧中引用的物件
  • 方法區類靜態屬性引用的物件 • 方法區常量池引用的物件
  • 本地方法棧 JNI 引用的物件
    雖然這些演算法可以判定一個物件是否能被回收,但是當滿足上述條 件時, 一個物件比不一定會被回收。當一個物件不可達 GC Root 時,這個物件並不會立馬被回收,而是出於一個死緩的階段,若要 被真正的回收需要經歷兩次標記.
    如果物件在可達性分析中沒有與 GC Root 的引用鏈,那麼此時就 會被第一次標記並且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當物件沒有覆蓋 finalize() 方法或者已被虛擬機器呼叫過,那麼就認為是沒必要的。 如果該物件有必要執行finalize()方法,那麼這個物件將會放在一個稱為 F-Queue 的對 佇列中,虛擬機器會
    觸發一個Finalize()執行緒去執行,此執行緒是低 優先順序的,並且虛擬機器不會承諾一直等待它執行完,這是因為如果 finalize() 執行緩慢或者發生了死鎖,那麼就會造成 F-Queue 佇列一直等待,造成了記憶體回收系統的崩潰。GC 對處於 F-Queue 中的物件進行第二次被標記,這時,該物件將被移除” 即將回收” 集合,等待回收。

7、垃圾回收的優點和原理)並考慮 2 種回收機制)

Java 語言中一個顯著的特點就是引入了垃圾回收機制,使 C++ 程式設計師最頭疼的記憶體管理的問題迎刃而解,它使得 Java 程式設計師在 編寫程式的時候不再需要考慮記憶體管理。由於有個垃圾回收機制, Java 中的物件不再有“作用域”的概念,只有物件的引用才有"作用域"。垃圾回收可以有效的 防止記憶體洩露,有效的使用可以使 用的記憶體。垃圾回收器通常是作為一個單獨的低級別的執行緒執行, 不可預知的情況下對記憶體堆中已經死亡的或者長時間沒有使用的 物件進行清楚和回收,程式設計師不能實時的呼叫垃圾回收器對某個對 象或所有物件進行垃圾回收。
回收機制有分代複製垃圾回收和標記垃圾回收,增量垃圾回收。

8、垃圾回收器的基本原理是什麼?垃圾回收器可以馬上回收記憶體嗎?有什麼辦法主動通知虛擬機器進行垃圾回收?

對於 GC 來說,當程式設計師建立物件時,GC 就開始監控這個物件 的地址、大小以及使用情況。通常,GC 採用有向圖的方式記錄和 管理堆(heap)中的所有物件。通過這種方式確定哪些物件是” 可達的”,哪些物件是”不可達的”。當 GC 確定一些物件為“不 可達”時,GC 就有責任回收這些記憶體空間。可以。程式設計師可以手動執行System.gc(),通知 GC 執行,但是Java 語言規範並不 保證 GC 一定會執行。

9、Java 中會存在記憶體洩漏嗎?請簡單描述)

所謂記憶體洩露就是指一個不再被程式使用的物件或變數一直被佔 據在記憶體中。Java 中有垃圾回收機制,它可以保證一物件不再被 引用的時候,即物件變成了孤兒的時候,物件將自動被垃圾回收器 從記憶體中清除掉。由於Java 使用有向圖的方式進行垃圾回收管理, 可以消除引用迴圈的問題, 例如有兩個物件,相互引用,只要它們 和根程序不可達的,那麼 GC 也是可以回收它們的,例如下面的 程式碼可以看到這種情況的記憶體回收:

*/
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 element

 s[--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 集合中單獨刪除當前 物件,造成記憶體洩露。

10、深拷貝和淺拷貝)

簡單來講就是複製、克隆。

Person p=new Person(“張三”);

淺拷貝就是對物件中的資料成員進行簡單賦值,如果存在動態成員或者指標就會報錯。
深拷貝就是對物件中存在的動態成員或指標重新開闢記憶體空間。

11、System.gc() 和 Runtime.gc() 會做什麼事情?

這兩個方法用來提示 JVM 要進行垃圾回收。但是,立即開始還是 延遲進行垃圾回收是取決於 JVM 的。

12、finalize() 方法什麼時候被呼叫?解構函式 (finalization) 的 目的是什麼?

垃圾回收器(garbage colector)決定回收某物件時,就會執行該 物件的finalize() 方法 但是在 Java 中很不幸,如果記憶體總是充 足的,那麼垃圾回收可能永遠不會進行,也就是說 filalize() 可能 永遠不被執行,顯然指望它做收尾工作是靠不住的。 那麼 finalize() 究竟是做什麼的呢? 它最主要的用途是回收特殊渠道 申請的記憶體。Java 程式有垃圾回收器,所以一般情況下記憶體問題 不用程式設計師操心。但有一種 JNI(Java Native Interface)呼叫non-Java 程式(C 或 C++), finalize() 的工作就是回收這部 分的記憶體。

13、如果物件的引用被置為 null?垃圾收集器是否會立即釋放物件佔 用的記憶體?

不會,在下一個垃圾回收週期中,這個物件將是可被回收的。

14、什麼是分散式垃圾回收(DGC)?它是如何工作的?

DGC 叫做分散式垃圾回收。RMI 使用 DGC 來做自動垃圾回收。 因為 RMI 包含了跨虛擬機器的遠端物件的引用,垃圾回收是很困難 的。DGC 使用引用計數演算法來給遠端物件提供自動記憶體管理。

15、序列(serial)收集器和吞吐量(throughput)收集器的區別 是什麼?

吞吐量收集器使用並行版本的新生代垃圾收集器,它用於中等規模 和大規模資料的應用程式。 而序列收集器對大多數的小應用(在 現代處理器上需要大概 100M 左右的記憶體)就足夠了。

16、在 Java 中?物件什麼時候可以被垃圾回收?

當物件對當前使用這個物件的應用程式變得不可觸及的時候,這個 物件就可以被回收了。

17、簡述 Java 記憶體分配與回收策率以及 Minor GC 和 Major GC)

  • 物件優先在堆的 Eden 區分配
  • 大物件直接進入老年代
  • 長期存活的物件將直接進入老年代
    當 Eden 區沒有足夠的空間進行分配時,虛擬機器會執行一次 Minor GC。Minor GC 通常發生在新生代的 Eden 區,在這個區 的物件生存期短,往往發生 Gc 的頻率較高,回收速度比較快; Full GC/Major GC 發生在老年代,一般情況下,觸發老年代 GC 的時候不會觸發 Minor GC,但是通過配置,可以在 Full GC 之 前進行一次 Minor GC 這樣可以加快老年代的回收速度。

18、JVM 的永久代中會發生垃圾回收麼?

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值, 會觸發完全垃圾回收(Full GC)。
注: Java 8 中已經移除了永久代,新加了一個叫做元資料區的 native 記憶體
區。

19、Java 中垃圾收集的方法有哪些?

標記 - 清除:這是垃圾收集演算法中最基礎的,根據名字就可以知 道,它的 思想就是標記哪些要被回收的物件,然後統一回收。這種 方法很簡單,但是會有兩個主要問題:

  1. 效率不高,標記和清除的效率都很低;
  2. 會產生大量不連續的記憶體碎片,導致以後程式在分配較大的物件時,由於沒有充足的連續記憶體而提前觸發一次 GC 動作。
    複製演算法:為了解決效率問題,複製演算法將可用記憶體按容量劃分為相等的兩部分,然後每次只使用其中的一塊,當一塊記憶體用完時,就將還存活的對 象複製到第二塊記憶體上,然後一次性清楚完第一塊記憶體,再將第二塊上的 物件複製到第一塊。但是這種方式,記憶體的代價太高,每次基本上都要浪 費一般的記憶體。
    於是將該演算法進行了改進,記憶體區域不再是按照 1:1 去劃分,而 是將記憶體劃分為 8:1:1 三部分,較大那份記憶體交 Eden 區,其餘 是兩塊較小的記憶體區叫 Survior 區。每次都會優先使用 Eden 區, 若 Eden 區滿,就將物件複製到第二塊記憶體區上,然後清除 Eden 區,如果此時存活的物件太多,以至於 Survivor 不夠時,會將這 些物件通過分配擔保機制複製到老年代中。(java 堆又分為新生代和老年代)
    標記 - 整理:該演算法主要是為了解決標記 - 清除,產生大量記憶體 碎片的問題;當物件存活率較高時,也解決了複製演算法的效率問題。 它的不同之處就是在清除物件的時候現將可回收物件移動到一端, 然後清除掉端邊界以外的物件,這樣就不會產生記憶體碎片了。
    分代收集:現在的虛擬機器垃圾收集大多采用這種方式,它根據物件的生存週期,將堆分為新生代和老年代。在新生代中,由於物件生存期短,每次回 收都會有大量物件死去,那麼這時就採用複製演算法。 老年代裡的物件存活率較高,沒有額外的空間進行分配擔保。

20、什麼是類載入器?類載入器有哪些?

實現通過類的許可權定名獲取該類的二進位制位元組流的程式碼塊叫做類載入器。 主要有一下四種類載入器:

  • 啟動類載入器(BootstrapClassLoader)用來載入Java核 心類庫,無法被
    Java 程式直接引用。
  • 擴充套件類載入器(extensions class loader):它用來載入 Java 的擴充套件庫。Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類 載入器在此目錄裡面查詢並載入 Java 類。
  • 系統類載入器(system class loader):它根據 Java 應用 的類路徑(CLASSPATH)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以通過 ClassLoader.getSystemClassLoader() 來獲取它。
  • 使用者自定義類載入器,通過繼承 java.lang.ClassLoader 類 的方式實現。

21、類載入器雙親委派模型機制?

當一個類收到了類載入請求時,不會自己先去載入這個類,而是將其委派給父類,由父類去載入,如果此時父類不能載入,反饋給子類,由子類去完成類的載入。