5_JVM面試題

JVM面試題

1. Java內存區域

1.1 JVM 的主要組成部分及其作用?

img

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KRoFAvfy-1593178863746)(C:/Users/luche/AppData/Roaming/Typora/typora-user-images/image-20200624150247451.png)]

img

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

  • Class loader(類裝載):根據給定的全限定名類名(如:java.lang.Object)來裝載class文件到運行時數據區中的方法區。

  • 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對象,用來封裝類在方法區內的數據結構。

1.2 JVM 運行時數據區

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

img
本地方法棧(Native Method Stack):與虛擬機棧的作用是一樣的,只不過虛擬機棧是服務 Java 方法的,而本地方法棧是爲虛擬機調用 Native 方法服務的;
Java 堆(Java Heap):Java 虛擬機中內存最大的一塊,是被所有線程共享的,幾乎所有的對象實例都在這裏分配內存;
方法區(Methed Area):用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。

程序計數器

程序計數器是一塊較小的內存空間,用來存放當前線程所執行的字節碼的位置(行號)。

爲了線程切換後能恢復到正確的執行位置,每條線程都有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

從上面的介紹中我們知道程序計數器主要有兩個作用:

  1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
  2. 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

注意:程序計數器是唯一一個不會出現 OutOfMemoryError 的內存區域,它的生命週期隨着線程的創建而創建,隨着線程的結束而死亡。

Java虛擬機棧

每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至執行完成的過程,對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。

與程序計數器一樣,Java 虛擬機棧(也叫線程棧)也是線程私有的,它的生命週期和線程相同,描述的是 Java 方法執行的內存模型,每次方法調用的數據都是通過棧傳遞的。

Java 內存可以粗糙的區分爲堆內存(Heap)和棧內存 (Stack),其中棧就是現在說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。(實際上,Java 虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。)

局部變量表主要存放了編譯器可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)。

Java 虛擬機棧會出現兩種錯誤:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態擴展,那麼當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 錯誤。
  • OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 錯誤。

Java 虛擬機棧也是線程私有的,每個線程都有各自的 Java 虛擬機棧,而且隨着線程的創建而創建,隨着線程的死亡而死亡。

擴展:那麼方法/函數如何調用?

Java 棧可用類比數據結構中棧,Java 棧中保存的主要內容是棧幀,每一次函數調用都會有一個對應的棧幀被壓入 Java 棧,每一個函數調用結束後,都會有一個棧幀被彈出。

Java 方法有兩種返回方式:

  1. return 語句。
  2. 拋出異常。

不管哪種返回方式都會導致棧幀被彈出。

本地方法棧

和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。

本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用於存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。

方法執行完畢後相應的棧幀也會出棧並釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種錯誤。

Java 虛擬機所管理的內存中最大的一塊,Java 堆是所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這裏分配內存。

Java世界中“幾乎”所有的對象都在堆中分配,但是,隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那麼“絕對”了。從jdk 1.7開始已經默認開啓逃逸分析,如果某些方法中的對象引用沒有被返回或者未被外面使用(也就是未逃逸出去),那麼對象可以直接在棧上分配內存。

Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集算法,所以 Java 堆還可以細分爲:新生代和老年代:再細緻一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。

在 JDK 7 版本及JDK 7 版本之前,堆內存被通常被分爲下面三部分:

  1. 新生代內存(Young Generation)
  2. 老生代(Old Generation)
  3. 永生代(Permanent Generation)

JVMå †å†å­˜ç»“æž„-JDK7

JDK 8 版本之後方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。

JVMå †å†å­˜ç»“æž„-JDK8

上圖所示的 Eden 區、兩個 Survivor 區都屬於新生代(爲了區分,這兩個 Survivor 區域按照順序被命名爲 from 和 to),中間一層屬於老年代。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-eLCHX6jl-1593178863756)(C:/Users/luche/AppData/Roaming/Typora/typora-user-images/image-20200624152557405.png)]

STW:垃圾回收的時候stop the world暫停用戶線程,讓垃圾線程專心的做垃圾回收

OOM: OutOfMemoryError

JVM調優的目的:減少STW的時間,就是減少用戶停頓的時間,stw在做gc的時候產生,就是要減少發生gc的次數。

大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,如果對象還存活,則會進入 s0 或者 s1,並且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增加到一定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。

修正(issue552):“Hotspot遍歷所有對象時,按照年齡從小到大對其所佔用的大小進行累積,當累積的某個年齡大小超過了survivor區的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,作爲新的晉升年齡閾值”。

堆這裏最容易出現的就是 OutOfMemoryError 錯誤,並且出現這種錯誤之後的表現形式還會有幾種,比如:

  1. OutOfMemoryError: GC Overhead Limit Exceeded : 當JVM花太多時間執行垃圾回收並且只能回收很少的堆空間時,就會發生此錯誤。
  2. java.lang.OutOfMemoryError: Java heap space :假如在創建新的對象時, 堆內存中的空間不足以存放新創建的對象, 就會引發java.lang.OutOfMemoryError: Java heap space 錯誤。(和本機物理內存無關,和你配置的內存大小有關!)

Java中棧和堆之間的關係

堆中分配的是對象,也就是new出來的東西。

棧中局部變量表主要存放了編譯器可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)。

preview

方法區

方法區與 Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然 Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。

方法區也被稱爲永久代。很多人都會分不清方法區和永久代的關係,爲此我也查閱了文獻。

方法區和永久代的關係

《Java 虛擬機規範》只是規定了有方法區這麼個概念和它的作用,並沒有規定如何去實現它。那麼,在不同的 JVM 上方法區的實現肯定是不同的了。 方法區和永久代的關係很像 Java 中接口和類的關係,類實現了接口,而永久代就是 HotSpot 虛擬機對虛擬機規範中方法區的一種實現方式。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機規範中的定義,是一種規範,而永久代是一種實現,一個是標準一個是實現,其他的虛擬機實現並沒有永久代這一說法。

常用參數

JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小

-XX:PermSize=N //方法區 (永久代) 初始大小
-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGenCopy to clipboardErrorCopied

相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入方法區後就“永久存在”了。

JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。

下面是一些常用參數:

-XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小Copy to clipboardErrorCopied

與永久代很大的不同就是,如果不指定大小的話,隨着更多類的創建,虛擬機會耗盡所有可用的系統內存。

爲什麼要將永久代 (PermGen) 替換爲元空間 (MetaSpace) 呢?
  1. 整個永久代有一個 JVM 本身設置固定大小上限,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,雖然元空間仍舊可能溢出,但是比原來出現的機率會更小。

    當你元空間溢出時會得到如下錯誤: java.lang.OutOfMemoryError: MetaSpace

你可以使用 -XX:MaxMetaspaceSize 標誌設置最大元空間大小,默認值爲 unlimited,這意味着它只受系統內存的限制。-XX:MetaspaceSize 調整標誌定義元空間的初始大小如果未指定此標誌,則 Metaspace 將根據運行時的應用程序需求動態地重新調整大小。

  1. 元空間裏面存放的是類的元數據,這樣加載多少類的元數據就不由 MaxPermSize 控制了, 而由系統的實際可用空間來控制,這樣能加載的類就更多了。
  2. 在 JDK8,合併 HotSpot 和 JRockit 的代碼時, JRockit 從來沒有一個叫永久代的東西, 合併之後就沒有必要額外的設置這麼一個永久代的地方了。

運行時常量池

運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池表(用於存放編譯期生成的各種字面量和符號引用)

既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 錯誤。

JDK1.7 及之後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。

修正(issue747reference):

  1. JDK1.7之前運行時常量池邏輯包含字符串常量池存放在方法區, 此時hotspot虛擬機對方法區的實現爲永久代
  2. JDK1.7 字符串常量池被從方法區拿到了堆中, 這裏沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區, 也就是hotspot中的永久代
  3. JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆, 運行時常量池還在方法區, 只不過方法區的實現從永久代變成了元空間(Metaspace)

相關問題:JVM 常量池中存儲的是對象引用: https://www.zhihu.com/question/57109429/answer/151717241 by RednaxelaFX

直接內存

直接內存並不是虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現。

JDK1.4 中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel)緩存區(Buffer) 的 I/O 方式,它可以直接使用 Native 函數庫直接分配堆外內存,然後通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因爲避免了在 Java 堆和 Native 堆之間來回複製數據

本機直接內存的分配不會受到 Java 堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

1.3 深拷貝和淺拷貝

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

深拷貝(deepCopy)是增加了一個指針並且申請了一個新的內存,使這個增加的指針指向這個新的內存,使用深拷貝的情況下,釋放內存的時候不會因爲出現淺拷貝時釋放同一個內存的錯誤。

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

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

1.4 說一下堆棧的區別?

物理地址

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

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

內存分別

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

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

存放的內容

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

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

PS:

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

程序的可見度

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

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

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

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

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

2. HotSpot虛擬機對象探祕

對象的創建

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

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

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

img

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

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

img

finalize方法一個對象只能執行一次,只能在第一次進入被回收的隊列,而且重寫了finalize方法纔會被執行。

爲對象分配內存

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

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

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

img

處理併發安全問題

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

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

img

對象的訪問定位

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

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

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

對象類型數據和對象實例數據

對象實例數據(堆):對象中各個實例字段的數據
對象類型數據(方法區):對象的類型、父類、實現的接口、方法等
靜態區(也在方法區中)用來存放靜態變量,靜態塊

句柄訪問

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

img

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

直接指針

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

img

內存溢出異常

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)是一種常見的基礎垃圾收集算法,它將垃圾收集分爲兩個階段:

  • 標記階段:標記出可以回收的對象。
  • 清除階段:回收被標記的對象所佔用的空間。
    標記-清除算法之所以是基礎的,是因爲後面講到的垃圾收集算法都是在此算法的基礎上進行改進的。

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

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

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

img

複製算法

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

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

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

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

img

標記-整理算法

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

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

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

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

img

分代收集算法

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

img

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

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

img

  • 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)時,升級爲老生代。大對象也會直接進入老生代。

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

內存分配策略

簡述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 對象。

img

類加載器分類:

  • 啓動類加載器(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 信息;
    父類去加載,如果此時父類不能加載,反饋給子類,由子類去完成類的加載。

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 詳細信息。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章