性能優化專題(二)JVM虛擬機

目錄

Java內存區域

說一下 JVM 的主要組成部分及其作用?

說一下 JVM 運行時數據區

深拷貝和淺拷貝

說一下堆棧的區別?

隊列和棧是什麼?有什麼區別?

HotSpot虛擬機對象探祕

對象的創建

爲對象分配內存

處理併發安全問題

​對象的訪問定位

句柄訪問

直接指針

內存溢出異常

Java會存在內存泄漏嗎?請簡單描述

垃圾收集器

簡述Java垃圾回收機制

GC是什麼?爲什麼要GC

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

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

Java 中都有哪些引用類型?

怎麼判斷對象是否可以被回收?

在Java中,對象什麼時候可以被垃圾回收

JVM中的永久代中會發生垃圾回收嗎

說一下 JVM 有哪些垃圾回收算法?

標記-清除算法

複製算法

標記-整理算法

分代收集算法

說一下 JVM 有哪些垃圾回收器?

詳細介紹一下 CMS 垃圾回收器?

新生代垃圾回收器和老年代垃圾回收器都有哪些?有什麼區別?

簡述分代垃圾回收器是怎麼工作的?

內存分配策略

簡述java內存分配與回收策率以及Minor GC和Major GC

對象優先在 Eden 區分配

大對象直接進入老年代

長期存活對象將進入老年代

虛擬機類加載機制

簡述java類加載機制?

描述一下JVM加載Class文件的原理機制

什麼是類加載器,類加載器有哪些?

說一下類裝載的執行過程?

什麼是雙親委派模型?

JVM調優

說一下 JVM 調優的工具?

常用的 JVM 調優的參數都有哪些?


Java內存區域

說一下 JVM 的主要組成部分及其作用?


JVM包含兩個子系統和兩個組件,兩個子系統爲Class loader(類裝載)、Execution engine(執行引擎);兩個組件爲Runtime data area(運行時數據區)、Native Interface(本地接口)。

  • Class loader(類裝載):根據給定的全限定名類名(如:java.lang.Object)來裝載class文件到Runtime data area中的method area。
  • Execution engine(執行引擎):執行classes中的指令。
  • Native Interface(本地接口):與native libraries交互,是其它編程語言交互的接口。
  • Runtime data area(運行時數據區域):這就是我們常說的JVM的內存。

作用 :首先通過編譯器把 Java 代碼轉換成字節碼,類加載器(ClassLoader)再把字節碼加載到內存中,將其放在運行時數據區(Runtime data area)的方法區內,而字節碼文件只是 JVM 的一套指令集規範,並不能直接交給底層操作系統去執行,因此需要特定的命令解析器執行引擎(Execution Engine),將字節碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實現整個程序的功能。

下面是Java程序運行機制詳細說明

Java程序運行機制步驟

  • 首先利用IDE集成開發工具編寫Java源代碼,源文件的後綴爲.java;
  • 再利用編譯器(javac命令)將源代碼編譯成字節碼文件,字節碼文件的後綴名爲.class;
  • 運行字節碼的工作是由解釋器(java命令)來完成的。


從上圖可以看,java文件通過編譯器變成了.class文件,接下來類加載器又將這些.class文件加載到JVM中。
其實可以一句話來解釋:類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class對象,用來封裝類在方法區內的數據結構。

說一下 JVM 運行時數據區

Java 虛擬機在執行 Java 程序的過程中會把它所管理的內存區域劃分爲若干個不同的數據區域。這些區域都有各自的用途,以及創建和銷燬的時間,有些區域隨着虛擬機進程的啓動而存在,有些區域則是依賴線程的啓動和結束而建立和銷燬。Java 虛擬機所管理的內存被劃分爲如下幾個區域:


不同虛擬機的運行時數據區可能略微有所不同,但都會遵從 Java 虛擬機規範, Java 虛擬機規範規定的區域分爲以下 5 個部分:

  • 程序計數器(Program Counter Register):當前線程所執行的字節碼的行號指示器,字節碼解析器的工作是通過改變這個計數器的值,來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能,都需要依賴這個計數器來完成;
  • Java 虛擬機棧(Java Virtual Machine Stacks):用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息;
  • 本地方法棧(Native Method Stack):與虛擬機棧的作用是一樣的,只不過虛擬機棧是服務 Java 方法的,而本地方法棧是爲虛擬機調用 Native 方法服務的;
  • Java 堆(Java Heap):Java 虛擬機中內存最大的一塊,是被所有線程共享的,幾乎所有的對象實例都在這裏分配內存;
  • 方法區(Methed Area):用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。

深拷貝和淺拷貝

淺拷貝(shallowCopy)只是增加了一個指針指向已存在的內存地址,

深拷貝(deepCopy)是增加了一個指針並且申請了一個新的內存,使這個增加的指針指向這個新的內存,

使用深拷貝的情況下,釋放內存的時候不會因爲出現淺拷貝時釋放同一個內存的錯誤。

淺複製:僅僅是指向被複制的內存地址,如果原地址發生改變,那麼淺複製出來的對象也會相應的改變。

深複製:在計算機中開闢一塊新的內存地址用於存放複製的對象。

說一下堆棧的區別?

物理地址

堆的物理地址分配對對象是不連續的。因此性能慢些。在GC的時候也要考慮到不連續的分配,所以有各種算法。比如,標記-消除,複製,標記-壓縮,分代(即新生代使用複製算法,老年代使用標記——壓縮)

棧使用的是數據結構中的棧,先進後出的原則,物理地址分配是連續的。所以性能快。

內存分別

堆因爲是不連續的,所以分配的內存是在運行期確認的,因此大小不固定。一般堆大小遠遠大於棧。

棧是連續的,所以分配的內存大小要在編譯期就確認,大小是固定的。

存放的內容

堆存放的是對象的實例和數組。因此該區更關注的是數據的存儲

棧存放:局部變量,操作數棧,返回結果。該區更關注的是程序方法的執行。

PS:

  1. 靜態變量放在方法區
  2. 靜態的對象還是放在堆。

程序的可見度

堆對於整個應用程序都是共享、可見的。

棧只對於線程是可見的。所以也是線程私有。他的生命週期和線程相同。

隊列和棧是什麼?有什麼區別?

隊列和棧都是被用來預存儲數據的。

  • 操作的名稱不同。隊列的插入稱爲入隊,隊列的刪除稱爲出隊。棧的插入稱爲進棧,棧的刪除稱爲出棧。
  • 可操作的方式不同。隊列是在隊尾入隊,隊頭出隊,即兩邊都可操作。而棧的進棧和出棧都是在棧頂進行的,無法對棧底直接進行操作。
  • 操作的方法不同。隊列是先進先出(FIFO),即隊列的修改是依先進先出的原則進行的。新來的成員總是加入隊尾(不能從中間插入),每次離開的成員總是隊列頭上(不允許中途離隊)。而棧爲後進先出(LIFO),即每次刪除(出棧)的總是當前棧中最新的元素,即最後插入(進棧)的元素,而最先插入的被放在棧的底部,要到最後才能刪除。

HotSpot虛擬機對象探祕

對象的創建

說到對象的創建,首先讓我們看看 Java 中提供的幾種對象創建方式:

Header 解釋
使用new關鍵字 調用了構造函數
使用Class的newInstance方法 調用了構造函數
使用Constructor類的newInstance方法 調用了構造函數
使用clone方法 沒有調用構造函數
使用反序列化 沒有調用構造函數

下面是對象創建的主要流程:

虛擬機遇到一條new指令時,先檢查常量池是否已經加載相應的類,如果沒有,必須先執行相應的類加載。類加載通過後,接下來分配內存。若Java堆中內存是絕對規整的,使用“指針碰撞“方式分配內存;如果不是規整的,就從空閒列表中分配,叫做”空閒列表“方式。劃分內存時還需要考慮一個問題-併發,也有兩種方式: CAS同步處理,或者本地線程分配緩衝(Thread Local Allocation Buffer, TLAB)。然後內存空間初始化操作,接着是做一些必要的對象設置(元信息、哈希碼…),最後執行<init>方法。

爲對象分配內存

類加載完成後,接着會在Java堆中劃分一塊內存分配給對象。內存分配根據Java堆是否規整,有兩種方式:

  • 指針碰撞:如果Java堆的內存是規整,即所有用過的內存放在一邊,而空閒的的放在另一邊。分配內存時將位於中間的指針指示器向空閒的內存移動一段與對象大小相等的距離,這樣便完成分配內存工作。
  • 空閒列表:如果Java堆的內存不是規整的,則需要由虛擬機維護一個列表來記錄那些內存是可用的,這樣在分配的時候可以從列表中查詢到足夠大的內存分配給對象,並在分配後更新列表記錄。

選擇哪種分配方式是由 Java 堆是否規整來決定的,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

處理併發安全問題

對象的創建在虛擬機中是一個非常頻繁的行爲,哪怕只是修改一個指針所指向的位置,在併發情況下也是不安全的,可能出現正在給對象 A 分配內存,指針還沒來得及修改,對象 B 又同時使用了原來的指針來分配內存的情況。解決這個問題有兩種方案:

  • 對分配內存空間的動作進行同步處理(採用 CAS + 失敗重試來保障更新操作的原子性);
  • 把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在 Java 堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer, TLAB)。哪個線程要分配內存,就在哪個線程的 TLAB 上分配。只有 TLAB 用完並分配新的 TLAB 時,才需要同步鎖。通過-XX:+/-UserTLAB參數來設定虛擬機是否使用TLAB。


對象的訪問定位

Java程序需要通過 JVM 棧上的引用訪問堆中的具體對象。對象的訪問方式取決於 JVM 虛擬機的實現。目前主流的訪問方式有 句柄直接指針 兩種方式。

指針: 指向對象,代表一個對象在內存中的起始地址。

句柄: 可以理解爲指向指針的指針,維護着對象的指針。句柄不直接指向對象,而是指向對象的指針(句柄不發生變化,指向固定內存地址),再由對象的指針指向對象的真實內存地址。

句柄訪問

Java堆中劃分出一塊內存來作爲句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數據對象類型數據各自的具體地址信息,具體構造如下圖所示:

優勢:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而引用本身不需要修改。

直接指針

如果使用直接指針訪問,引用 中存儲的直接就是對象地址,那麼Java堆對象內部的佈局中就必須考慮如何放置訪問類型數據的相關信息。

優勢:速度更快,節省了一次指針定位的時間開銷。由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是非常可觀的執行成本。HotSpot 中採用的就是這種方式。

內存溢出異常

Java會存在內存泄漏嗎?請簡單描述

內存泄漏是指不再被使用的對象或者變量一直被佔據在內存中。理論上來說,Java是有GC垃圾回收機制的,也就是說,不再被使用的對象,會被GC自動回收掉,自動從內存中清除。

但是,即使這樣,Java也還是存在着內存泄漏的情況,java導致內存泄露的原因很明確:長生命週期的對象持有短生命週期對象的引用就很可能發生內存泄露,儘管短生命週期對象已經不再需要,但是因爲長生命週期對象持有它的引用而導致不能被回收,這就是java中內存泄露的發生場景。

垃圾收集器

簡述Java垃圾回收機制

在java中,程序員是不需要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在JVM中,有一個垃圾回收線程,它是低優先級的,在正常情況下是不會執行的,只有在虛擬機空閒或者當前堆內存不足時,纔會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。

GC是什麼?爲什麼要GC

GC 是垃圾收集的意思(Gabage Collection),內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存

回收會導致程序或系統的不穩定甚至崩潰,Java 提供的 GC 功能可以自動監測對象是否超過作用域從而達到自動

回收內存的目的,Java 語言沒有提供釋放已分配內存的顯示操作方法。

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

java語言最顯著的特點就是引入了垃圾回收機制,它使java程序員在編寫程序時不再考慮內存管理的問題。

由於有這個垃圾回收機制,java中的對象不再有“作用域”的概念,只有引用的對象纔有“作用域”。

垃圾回收機制有效的防止了內存泄露,可以有效的使用可使用的內存。

垃圾回收器通常作爲一個單獨的低級別的線程運行,在不可預知的情況下對內存堆中已經死亡的或很長時間沒有用過的對象進行清除和回收。

程序員不能實時的對某個對象或所有對象調用垃圾回收器進行垃圾回收。

垃圾回收有分代複製垃圾回收、標記垃圾回收、增量垃圾回收。

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

對於GC來說,當程序員創建對象時,GC就開始監控這個對象的地址、大小以及使用情況。

通常,GC採用有向圖的方式記錄和管理堆(heap)中的所有對象。通過這種方式確定哪些對象是"可達的",哪些對象是"不可達的"。當GC確定一些對象爲"不可達"時,GC就有責任回收這些內存空間。

可以。程序員可以手動執行System.gc(),通知GC運行,但是Java語言規範並不保證GC一定會執行。

Java 中都有哪些引用類型?

  • 強引用:發生 gc 的時候不會被回收。
  • 軟引用:有用但不是必須的對象,在發生內存溢出之前會被回收。
  • 弱引用:有用但不是必須的對象,在下一次GC時會被回收。
  • 虛引用(幽靈引用/幻影引用):無法通過虛引用獲得對象,用 PhantomReference 實現虛引用,虛引用的用途是在 gc 時返回一個通知。

怎麼判斷對象是否可以被回收?

垃圾收集器在做垃圾回收的時候,首先需要判定的就是哪些內存是需要被回收的,哪些對象是「存活」的,是不可以被回收的;哪些對象已經「死掉」了,需要被回收。

一般有兩種方法來判斷:

  • 引用計數器法:爲每個對象創建一個引用計數,有對象引用時計數器 +1,引用被釋放時計數 -1,當計數器爲 0 時就可以被回收。它有一個缺點不能解決循環引用的問題;
  • 可達性分析算法:從 GC Roots 開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是可以被回收的。

在Java中,對象什麼時候可以被垃圾回收

當對象對當前使用這個對象的應用程序變得不可觸及的時候,這個對象就可以被回收了。
垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是爲什麼正確的永久代大小對避免Full GC是非常重要的原因。

JVM中的永久代中會發生垃圾回收嗎

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是爲什麼正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到元數據區
(譯者注:Java8中已經移除了永久代,新加了一個叫做元數據區的native內存區)

說一下 JVM 有哪些垃圾回收算法?

  • 標記-清除算法:標記無用對象,然後進行清除回收。缺點:效率不高,無法清除垃圾碎片。
  • 複製算法:按照容量劃分二個大小相等的內存區域,當一塊用完的時候將活着的對象複製到另一塊上,然後再把已使用的內存空間一次清理掉。缺點:內存使用率不高,只有原來的一半。
  • 標記-整理算法:標記無用對象,讓所有存活的對象都向一端移動,然後直接清除掉端邊界以外的內存。
  • 分代算法:根據對象存活週期的不同將內存劃分爲幾塊,一般是新生代和老年代,新生代基本採用複製算法,老年代採用標記整理算法。

標記-清除算法

標記無用對象,然後進行清除回收。

標記-清除算法(Mark-Sweep)是一種常見的基礎垃圾收集算法,它將垃圾收集分爲兩個階段:

  • 標記階段:標記出可以回收的對象。
  • 清除階段:回收被標記的對象所佔用的空間。

標記-清除算法之所以是基礎的,是因爲後面講到的垃圾收集算法都是在此算法的基礎上進行改進的。

優點:實現簡單,不需要對象進行移動。

缺點:標記、清除過程效率低,產生大量不連續的內存碎片,提高了垃圾回收的頻率。

標記-清除算法的執行的過程如下圖所示

複製算法

爲了解決標記-清除算法的效率不高的問題,產生了複製算法。它把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾收集時,遍歷當前使用的區域,把存活對象複製到另外一個區域中,最後將當前使用的區域的可回收的對象進行回收。

優點:按順序分配內存即可,實現簡單、運行高效,不用考慮內存碎片。

缺點:可用的內存大小縮小爲原來的一半,對象存活率高時會頻繁進行復制。

複製算法的執行過程如下圖所示

標記-整理算法

在新生代中可以使用複製算法,但是在老年代就不能選擇複製算法了,因爲老年代的對象存活率會較高,這樣會有較多的複製操作,導致效率變低。標記-清除算法可以應用在老年代中,但是它效率不高,在內存回收後容易產生大量內存碎片。因此就出現了一種標記-整理算法(Mark-Compact)算法,與標記-整理算法不同的是,在標記可回收的對象後將所有存活的對象壓縮到內存的一端,使他們緊湊的排列在一起,然後對端邊界以外的內存進行回收。回收後,已用和未用的內存都各自一邊。

優點:解決了標記-清理算法存在的內存碎片問題。

缺點:仍需要進行局部對象移動,一定程度上降低了效率。

標記-整理算法的執行過程如下圖所示

分代收集算法

當前商業虛擬機都採用分代收集的垃圾收集算法。分代收集算法,顧名思義是根據對象的存活週期將內存劃分爲幾塊。一般包括年輕代、老年代 永久代(JDK8移除),如圖所示:

說一下 JVM 有哪些垃圾回收器?

如果說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。下圖展示了7種作用於不同分代的收集器,其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用於回收整個Java堆的G1收集器。不同收集器之間的連線表示它們可以搭配使用。

  • Serial收集器(複製算法): 新生代單線程收集器,標記和清理都是單線程,優點是簡單高效;
  • ParNew收集器 (複製算法): 新生代收並行集器,實際上是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現;
  • Parallel Scavenge收集器 (複製算法): 新生代並行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用戶線程時間/(用戶線程時間+GC線程時間),高吞吐量可以高效率的利用CPU時間,儘快完成程序的運算任務,適合後臺應用等對交互相應要求不高的場景;
  • Serial Old收集器 (標記-整理算法): 老年代單線程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (標記-整理算法): 老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(標記-清除算法): 老年代並行收集器,以獲取最短回收停頓時間爲目標的收集器,具有高併發、低停頓的特點,追求最短GC回收停頓時間。
  • G1(Garbage First)收集器 (標記-整理算法): Java堆並行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基於“標記-整理”算法實現,也就是說不會產生內存碎片。此外,G1收集器不同於之前的收集器的一個重要特點是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。

詳細介紹一下 CMS 垃圾回收器?

CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量爲代價來獲得最短回收停頓時間的垃圾回收器。對於要求服務器響應速度的應用上,這種垃圾回收器非常適合。在啓動 JVM 的參數加上“-XX:+UseConcMarkSweepGC”來指定使用 CMS 垃圾回收器。

CMS 使用的是標記-清除的算法實現的,所以在 gc 的時候回產生大量的內存碎片,當剩餘內存不能滿足程序運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會採用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低。

新生代垃圾回收器和老年代垃圾回收器都有哪些?有什麼區別?

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代垃圾回收器一般採用的是複製算法,複製算法的優點是效率高,缺點是內存利用率低;老年代回收器一般採用的是標記-整理的算法進行垃圾回收。

簡述分代垃圾回收器是怎麼工作的?

分代回收器有兩個分區:老生代和新生代,新生代默認的空間佔比總空間的 1/3,老生代的默認佔比是 2/3。

新生代使用的是複製算法,新生代裏有 3 個分區:Eden、To Survivor、From Survivor,它們的默認佔比是 8:1:1,它的執行流程如下:

  • 把 Eden + From Survivor 存活的對象放入 To Survivor 區;
  • 清空 Eden 和 From Survivor 分區;
  • From Survivor 和 To Survivor 分區交換,From Survivor 變 To Survivor,To Survivor 變 From Survivor。

每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級爲老生代。大對象也會直接進入老生代。

老生代當空間佔用到達某個值之後就會觸發全局垃圾收回(Full GC),一般使用標記整理的執行算法。以上這些循環往復就構成了整個分代垃圾回收的整體執行流程。

內存分配策略

簡述java內存分配與回收策率以及Minor GC和Major GC

所謂自動內存管理,最終要解決的也就是內存分配和內存回收兩個問題。前面我們介紹了內存回收,這裏我們再來聊聊內存分配。

對象的內存分配通常是在 Java 堆上分配(隨着虛擬機優化技術的誕生,某些場景下也會在棧上分配,後面會詳細介紹),對象主要分配在新生代的 Eden 區,如果啓動了本地線程緩衝,將按照線程優先在 TLAB 上分配。少數情況下也會直接在老年代上分配。總的來說分配規則不是百分百固定的,其細節取決於哪一種垃圾收集器組合以及虛擬機相關參數有關,但是虛擬機對於內存的分配還是會遵循以下幾種「普世」規則:

對象優先在 Eden 區分配

多數情況,對象都在新生代 Eden 區分配。當 Eden 區分配沒有足夠的空間進行分配時,虛擬機將會發起一次 Minor GC。如果本次 GC 後還是沒有足夠的空間,則將啓用分配擔保機制在老年代中分配內存。

這裏我們提到 Minor GC,如果你仔細觀察過 GC 日常,通常我們還能從日誌中發現 Major GC/Full GC。

  • Minor GC 是指發生在新生代的 GC,因爲 Java 對象大多都是朝生夕死,所有 Minor GC 非常頻繁,一般回收速度也非常快;
  • Major GC/Full GC 是指發生在老年代的 GC,出現了 Major GC 通常會伴隨至少一次 Minor GC。Major GC 的速度通常會比 Minor GC 慢 10 倍以上。

大對象直接進入老年代

所謂大對象是指需要大量連續內存空間的對象,頻繁出現大對象是致命的,會導致在內存還有不少空間的情況下提前觸發 GC 以獲取足夠的連續空間來安置新對象。

前面我們介紹過新生代使用的是標記-清除算法來處理垃圾回收的,如果大對象直接在新生代分配就會導致 Eden 區和兩個 Survivor 區之間發生大量的內存複製。因此對於大對象都會直接在老年代進行分配。

長期存活對象將進入老年代

虛擬機採用分代收集的思想來管理內存,那麼內存回收時就必須判斷哪些對象應該放在新生代,哪些對象應該放在老年代。因此虛擬機給每個對象定義了一個對象年齡的計數器,如果對象在 Eden 區出生,並且能夠被 Survivor 容納,將被移動到 Survivor 空間中,這時設置對象年齡爲 1。對象在 Survivor 區中每「熬過」一次 Minor GC 年齡就加 1,當年齡達到一定程度(默認 15) 就會被晉升到老年代。

虛擬機類加載機制

簡述java類加載機制?

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,解析和初始化,最終形成可以被虛擬機直接使用的java類型。

描述一下JVM加載Class文件的原理機制

Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬盤讀取到內存中。在寫程序的時候,我們幾乎不需要關心類的加載,因爲這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。

類裝載方式,有兩種 :

  1. 隱式裝載, 程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中,
  2. 顯式裝載, 通過class.forname()等方法,顯式加載需要的類

Java類的加載是動態的,它並不會一次性將所有類全部加載後再運行,而是保證程序運行的基礎類(像是基類)完全加載到jvm中,至於其他類,則在需要的時候才加載。這當然就是爲了節省內存開銷。

什麼是類加載器,類加載器有哪些?

實現通過類的權限定名獲取該類的二進制字節流的代碼塊叫做類加載器。

主要有一下四種類加載器:

  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類的方式實現。

說一下類裝載的執行過程?

類裝載分爲以下 5 個步驟:

  • 加載:根據查找路徑找到相應的 class 文件然後導入;
  • 驗證:檢查加載的 class 文件的正確性;
  • 準備:給類中的靜態變量分配內存空間;
  • 解析:虛擬機將常量池中的符號引用替換成直接引用的過程。符號引用就理解爲一個標示,而在直接引用直接指向內存中的地址;
  • 初始化:對靜態變量和靜態代碼塊執行初始化工作。

什麼是雙親委派模型?

在介紹雙親委派模型之前先說下類加載器。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立在 JVM 中的唯一性,每一個類加載器,都有一個獨立的類名稱空間。類加載器就是根據指定全限定名稱將 class 文件加載到 JVM 內存,然後再轉化爲 class 對象。

類加載器分類:

  • 啓動類加載器(Bootstrap ClassLoader),是虛擬機自身的一部分,用來加載Java_HOME/lib/目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中並且被虛擬機識別的類庫;
  • 其他類加載器:
  • 擴展類加載器(Extension ClassLoader):負責加載\lib\ext目錄或Java. ext. dirs系統變量指定的路徑中的所有類庫;
  • 應用程序類加載器(Application ClassLoader)。負責加載用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器。一般情況,如果我們沒有自定義類加載器默認就是用這個加載器。

雙親委派模型:如果一個類加載器收到了類加載的請求,它首先不會自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣所有的加載請求都會被傳送到頂層的啓動類加載器中,只有當父加載無法完成加載請求(它的搜索範圍中沒找到所需的類)時,子加載器纔會嘗試去加載類。

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

JVM調優

說一下 JVM 調優的工具?

JDK 自帶了很多監控工具,都位於 JDK 的 bin 目錄下,其中最常用的是 jconsole 和 jvisualvm 這兩款視圖監控工具。

  • jconsole:用於對 JVM 中的內存、線程和類等進行監控;
  • jvisualvm:JDK 自帶的全能分析工具,可以分析:內存快照、線程快照、程序死鎖、監控內存的變化、gc 變化等。

常用的 JVM 調優的參數都有哪些?

  • -Xms2g:初始化推大小爲 2g;
  • -Xmx2g:堆最大內存爲 2g;
  • -XX:NewRatio=4:設置年輕的和老年代的內存比例爲 1:4;
  • -XX:SurvivorRatio=8:設置新生代 Eden 和 Survivor 比例爲 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器組合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器組合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器組合;
  • -XX:+PrintGC:開啓打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 詳細信息。

參考文檔:https://thinkwon.blog.csdn.net/article/details/104390752

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章