10個經典又容易被人疏忽的JVM面試題
前言
整理了10個經典又容易被疏忽的JVM面試題,謝謝閱讀,大家加油哈
github地址,感謝每顆star
https://github.com/whx123/JavaHome
1. 對象一定分配在堆中嗎?有沒有了解逃逸分析技術?
「對象一定分配在堆中嗎?」 不一定的,JVM通過「逃逸分析」,那些逃不出方法的對象會在棧上分配。
「什么是逃逸分析?」
逃逸分析(Escape Analysis),是一種可以有效減少Java 程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍,從而決定是否要將這個對象分配到堆上。
逃逸分析是指分析指針動態范圍的方法,它同編譯器優化原理的指針分析和外形分析相關聯。當變量(或者對象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會被其他方法或者線程所引用,這種現象稱作指針(或者引用)的逃逸(Escape)。通俗點講,如果一個對象的指針被多個方法或者線程引用時,那么我們就稱這個對象的指針發生了逃逸。
「一個逃逸分析的例子」
- /**
- * @author 撿田螺的小男孩
- */
- public class EscapeAnalysisTest {
- public static Object object;
- //StringBuilder可能被其他方法改變,逃逸到了方法外部。
- public StringBuilder escape(String a, String b) {
- //公眾號:撿田螺的小男孩
- StringBuilder str = new StringBuilder();
- str.append(a);
- str.append(b);
- return str;
- }
- //不直接返回StringBuffer,不發生逃逸
- public String notEscape(String a, String b) {
- //公眾號:撿田螺的小男孩
- StringBuilder str = new StringBuilder();
- str.append(a);
- str.append(b);
- return str.toString();
- }
- //外部線程可見object,發生逃逸
- public void objectEscape(){
- object = new Object();
- }
- //僅方法內部可見,不發生逃逸
- public void objectNotEscape(){
- Object object = new Object();
- }
- }
「逃逸分析的好處」
- 棧上分配,可以降低垃圾收集器運行的頻率。
- 同步消除,如果發現某個對象只能從一個線程可訪問,那么在這個對象上的操作可以不需要同步。
- 標量替換,把對象分解成一個個基本類型,并且內存分配不再是分配在堆上,而是分配在棧上。這樣的好處有,一、減少內存使用,因為不用生成對象頭。二、程序內存回收效率高,并且GC頻率也會減少。
2.虛擬機為什么使用元空間替換了永久代?
「什么是元空間?什么是永久代?為什么用元空間代替永久代?」 我們先回顧一下「方法區」吧,看看虛擬機運行時數據內存圖,如下:
方法區和堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯后的代碼等數據。
「什么是永久代?它和方法區有什么關系呢?」
如果在HotSpot虛擬機上開發、部署,很多程序員都把方法區稱作永久代。可以說方法區是規范,永久代是Hotspot針對該規范進行的實現。在Java7及以前的版本,方法區都是永久代實現的。
「什么是元空間?它和方法區有什么關系呢?」
對于Java8,HotSpots取消了永久代,取而代之的是元空間(Metaspace)。換句話說,就是方法區還是在的,只是實現變了,從永久代變為元空間了。
「為什么使用元空間替換了永久代?」
「永久代」是通過以下這兩個參數配置大小的~
- -XX:PremSize:設置永久代的初始大小
- -XX:MaxPermSize: 設置永久代的最大值,默認是64M
對于「永久代」,如果動態生成很多class的話,就很可能出現「java.lang.OutOfMemoryError: PermGen space錯誤」,因為永久代空間配置有限嘛。最典型的場景是,在web開發比較多jsp頁面的時候。
- JDK8之后,方法區存在于元空間(Metaspace)。物理內存不再與堆連續,而是直接存在于本地內存中,理論上機器「內存有多大,元空間就有多大」。
可以通過以下的參數來設置元空間的大小:
- -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時,適當提高該值。
- -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
- -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空間容量的百分比,減少為分配空間所導致的垃圾收集
- -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空間容量的百分比,減少為釋放空間所導致的垃圾收集
「所以,為什么使用元空間替換永久代?」
表面上看是為了避免OOM異常。因為通常使用PermSize和MaxPermSize設置永久代的大小就決定了永久代的上限,但是不是總能知道應該設置為多大合適, 如果使用默認值很容易遇到OOM錯誤。當使用元空間時,可以加載多少類的元數據就不再由MaxPermSize控制, 而由系統的實際可用空間來控制啦。
3.什么是Stop The World ? 什么是OopMap?什么是安全點?
進行垃圾回收的過程中,會涉及對象的移動。為了保證對象引用更新的正確性,必須暫停所有的用戶線程,像這樣的停頓,虛擬機設計者形象描述為「Stop The World」。
在HotSpot中,有個數據結構(映射表)稱為「OopMap」。一旦類加載動作完成的時候,HotSpot就會把對象內什么偏移量上是什么類型的數據計算出來,記錄到OopMap。在即時編譯過程中,也會在「特定的位置」生成 OopMap,記錄下棧上和寄存器里哪些位置是引用。
這些特定的位置主要在:
- 1.循環的末尾(非 counted 循環)
- 2.方法臨返回前 / 調用方法的call指令后
- 3.可能拋異常的位置
這些位置就叫作「安全點(safepoint)。」 用戶程序執行時并非在代碼指令流的任意位置都能夠在停頓下來開始垃圾收集,而是必須是執行到安全點才能夠暫停。
4.說一下JVM 的主要組成部分及其作用?
JVM包含兩個子系統和兩個組件,分別為
- Class loader(類裝載子系統)
- Execution engine(執行引擎子系統);
- Runtime data area(運行時數據區組件)
- Native Interface(本地接口組件)。
- 「Class loader(類裝載):」 根據給定的全限定名類名(如:java.lang.Object)來裝載class文件到運行時數據區的方法區中。
- 「Execution engine(執行引擎)」:執行class的指令。
- 「Native Interface(本地接口):」 與native lib交互,是其它編程語言交互的接口。
- 「Runtime data area(運行時數據區域)」:即我們常說的JVM的內存。
首先通過編譯器把 Java源代碼轉換成字節碼,Class loader(類裝載)再把字節碼加載到內存中,將其放在運行時數據區的方法區內,而字節碼文件只是 JVM 的一套指令集規范,并不能直接交給底層操作系統去執行,因此需要特定的命令解析器執行引擎(Execution Engine),將字節碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實現整個程序的功能。
5. 守護線程是什么?守護線程和非守護線程的區別是?守護線程的作用是?
「守護線程」是區別于用戶線程哈,「用戶線程」即我們手動創建的線程,而守護線程是程序運行的時候在后臺提供一種「通用服務的線程」。垃圾回收線程就是典型的守護線程。
「守護線程和非守護線程的區別是?」 我們通過例子來看吧~
- /**
- * 關注公眾號:撿田螺的小男孩
- */
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(()-> {
- while (true) {
- try {
- Thread.sleep(1000);
- System.out.println("我是子線程(用戶線程.I am running");
- } catch (Exception e) {
- }
- }
- });
- //標記為守護線程
- t1.setDaemon(true);
- //啟動線程
- t1.start();
- Thread.sleep(3000);
- System.out.println("主線程執行完畢...");
- }
運行結果:
可以發現標記為守護線程后,「主線程銷毀停止,守護線程一起銷毀」。我們再看下,去掉 t1.setDaemon(true)守護標記的效果:
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(()-> {
- while (true) {
- try {
- Thread.sleep(1000);
- System.out.println("我是子線程(用戶線程.I am running");
- } catch (Exception e) {
- }
- }
- });
- //啟動線程
- t1.start();
- Thread.sleep(3000);
- System.out.println("主線程執行完畢...");
- }
所以,當主線程退出時,JVM 也跟著退出運行,守護線程同時也會被回收,即使是死循環。如果是用戶線程,它會一直停在死循環跑。這就是「守護線程和非守護線程的區別」啦。
守護線程擁有「自動結束自己生命周期的特性」,非守護線程卻沒有。如果垃圾回收線程是非守護線程,當JVM 要退出時,由于垃圾回收線程還在運行著,導致程序無法退出,這就很尷尬。這就是「為什么垃圾回收線程需要是守護線程啦」。
6.WeakHashMap了解過嘛?它是怎么工作的?
「WeakHashMap」 類似HashMap ,不同點在WeakHashMap的key是「弱引用」的key。
談到「弱引用」,在這里回顧下四種引用吧
- 強引用:Object obj=new Object()這種,只要強引用關系還存在,垃圾收集器就永遠不會回收掉被引用的對象。
- 軟引用: 一般情況不會回收,如果內存不夠要溢出時才會進行回收
- 弱引用:當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
- 虛引用:為一個對象設置虛引用的唯一目的只是為了能在這個對象被回收時收到一個系統的通知。
正是因為WeakHashMap使用的是弱引用,「它的對象可能隨時被回收」。WeakHashMap 類的行為部分「取決于垃圾回收器的動作」,調用兩次size()方法返回不同值,調用兩次isEmpty(),一次返回true,一次返回false都是「可能的」。
WeakHashMap「工作原理」回答這兩點:
- WeakHashMap具有弱引用的特點:隨時被回收對象。
- 發生GC時,WeakHashMap是如何將Entry移除的呢?
WeakHashMap內部的Entry繼承了WeakReference,即弱引用,所以就具有了弱引用的特點,「隨時可能被回收」。看下源碼哈:
- private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
- V value;
- final int hash;
- Entry<K,V> next;
- /**
- * Creates new entry.
- */
- Entry(Object key, V value,
- ReferenceQueue<Object> queue,
- int hash, Entry<K,V> next) {
- super(key, queue);
- this.value = value;
- this.hash = hash;
- this.next = next;
- }
- ......
「WeakHashMap是如何將Entry移除的?」 GC每次清理掉一個對象之后,引用對象會放到ReferenceQueue的,接著呢遍歷queue進行刪除。WeakHashMap的增刪改查操作,就是直接/間接調用expungeStaleEntries()方法,達到及時清除過期entry的目的。可以看下expungeStaleEntries源碼哈:
- /**
- * Expunges stale entries from the table.
- */
- private void expungeStaleEntries() {
- for (Object x; (x = queue.poll()) != null; ) {
- synchronized (queue) {
- @SuppressWarnings("unchecked")
- Entry<K,V> e = (Entry<K,V>) x;
- int i = indexFor(e.hash, table.length);
- Entry<K,V> prev = table[i];
- Entry<K,V> p = prev;
- while (p != null) {
- Entry<K,V> next = p.next;
- if (p == e) {
- if (prev == e)
- table[i] = next;
- else
- prev.next = next;
- // Must not null out e.next;
- // stale entries may be in use by a HashIterator
- e.value = null; // Help GC
- size--;
- break;
- }
- prev = p;
- p = next;
- }
- }
- }
- }
7. 是否了解Java語法糖嘛?說下12種Java中常用的語法糖?
語法糖(Syntactic Sugar),也稱糖衣語法,讓程序更加簡潔,有更高的可讀性。Java 中最常用的語法糖主要有泛型、變長參數、條件編譯、自動拆裝箱、內部類等12種。
- 語法糖一、switch 支持 String 與枚舉
- 語法糖二、 泛型
- 語法糖三、 自動裝箱與拆箱
- 語法糖四 、 方法變長參數
- 語法糖五 、 枚舉
- 語法糖六 、 內部類
- 語法糖七 、條件編譯
- 語法糖八 、 斷言
- 語法糖九 、 數值字面量
- 語法糖十 、 for-each
- 語法糖十一 、 try-with-resource
- 語法糖十二、Lambda表達式
8. 什么是指針碰撞?什么是空閑列表?什么是TLAB?
一般情況下,JVM的對象都放在堆內存中(發生逃逸分析除外)。當類加載檢查通過后,Java虛擬機開始為新生對象分配內存。如果Java堆中內存是絕對規整的,所有被使用過的的內存都被放到一邊,空閑的內存放到另外一邊,中間放著一個指針作為分界點的指示器,所分配內存僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的實例,這種分配方式就是“「指針碰撞」”。
如果Java堆內存中的內存并不是規整的,已被使用的內存和空閑的內存相互交錯在一起,不可以進行指針碰撞啦,虛擬機必須維護一個列表,記錄哪些內存是可用的,在分配的時候從列表找到一塊大的空間分配給對象實例,并更新列表上的記錄,這種分配方式就是“「空閑列表」”
?對象創建在虛擬機中是非常頻繁的行為,可能存在線性安全問題。如果一個線程正在給A對象分配內存,指針還沒有來的及修改,同時另一個為B對象分配內存的線程,仍引用這之前的指針指向,這就出「問題」了。
可以把內存分配的動作按照線程劃分在不同的空間之中進行,每個線程在Java堆中預先分配一小塊內存,這就是「TLAB(Thread Local Allocation Buffer,本地線程分配緩存)」 。虛擬機通過-XX:UseTLAB設定它的。
9.CMS垃圾回收器的工作過程,CMS收集器和G1收集器的區別。
CMS(Concurrent Mark Sweep) 收集器:是一種以獲得最短回收停頓時間為目標的收集器,標記清除算法,運作過程:「初始標記,并發標記,重新標記,并發清除」,收集結束會產生大量空間碎片。如圖(下圖來源互聯網):
「CMS收集器和G1收集器的區別:」
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
- G1收集器收集范圍是老年代和新生代,不需要結合其他收集器使用;
- CMS收集器以最小的停頓時間為目標的收集器;
- G1收集器可預測垃圾回收的停頓時間
- CMS收集器是使用“標記-清除”算法進行的垃圾回收,容易產生內存碎片
- G1收集器使用的是“標記-整理”算法,進行了空間整合,降低了內存空間碎片。
10.JVM 調優
JVM調優其實就是通過調節JVM參數,即對垃圾收集器和內存分配的調優,以達到更高的吞吐和性能。JVM調優主要調節以下參數
「堆棧內存相關」
- -Xms 設置初始堆的大小
- -Xmx 設置最大堆的大小
- -Xmn 設置年輕代大小,相當于同時配置-XX:NewSize和-XX:MaxNewSize為一樣的值
- -Xss 每個線程的堆棧大小
- -XX:NewSize 設置年輕代大小(for 1.3/1.4)
- -XX:MaxNewSize 年輕代最大值(for 1.3/1.4)
- -XX:NewRatio 年輕代與年老代的比值(除去持久代)
- -XX:SurvivorRatio Eden區與Survivor區的的比值
- -XX:PretenureSizeThreshold 當創建的對象超過指定大小時,直接把對象分配在老年代。
- -XX:MaxTenuringThreshold設定對象在Survivor復制的最大年齡閾值,超過閾值轉移到老年代
「垃圾收集器相關」
- -XX:+UseParallelGC:選擇垃圾收集器為并行收集器。
- -XX:ParallelGCThreads=20:配置并行收集器的線程數
- -XX:+UseConcMarkSweepGC:設置年老代為并發收集。
- -XX:CMSFullGCsBeforeCompaction=5 由于并發收集器不對內存空間進行壓縮、整理,所以運行一段時間以后會產生“碎片”,使得運行效率降低。此值設置運行5次GC以后對內存空間進行壓縮、整理。
- -XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除碎片
「輔助信息相關」
- -XX:+PrintGCDetails 打印GC詳細信息
- -XX:+HeapDumpOnOutOfMemoryError讓JVM在發生內存溢出的時候自動生成內存快照,排查問題用
- -XX:+DisableExplicitGC禁止系統System.gc(),防止手動誤觸發FGC造成問題.
- -XX:+PrintTLAB 查看TLAB空間的使用情況
參考與感謝
- [JVM的逃逸分析] (https://segmentfault.com/a/1190000023475016)
- [面試官 | JVM 為什么使用元空間替換了永久代?] (https://my.oschina.net/u/3471412/blog/4426430)
- [Metaspace 之一:Metaspace整體介紹(永久代被替換原因、元空間特點、元空間內存查看分析方法)] (https://www.cnblogs.com/duanxz/p/3520829.html)
- [深入理解WeakHashmap] (https://blog.51cto.com/mikewang/880775)
- [一文搞懂WeakHashMap工作原理] (https://baijiahao.baidu.com/s?id=1666368292461068600&wfr=spider&for=pc)
- [談談什么是守護線程及作用] (https://www.cnblogs.com/quanxiaoha/p/10731361.html)
- [淺析java中的TLAB] (https://www.jianshu.com/p/8be816cbb5ed)
- 《深入理解Java虛擬機》
本文轉載自微信公眾號「撿田螺的小男孩」,可以通過以下二維碼關注。轉載本文請聯系撿田螺的小男孩公眾號。