金三銀四面試季節之Java 核心面試技術點 - JVM 小結

更多關於Java的文章請訪問:GitHub

描述一下 JVM 的內存區域

  • 程序計數器(PC,Program Counter Register)。在 JVM 規範中,每一個線程都有它本身的程序計數器,而且任什麼時候間一個線程都只有一個方法在執行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執行的 Java 方法的 JVM 指令地址;或者,若是是在執行本地方法,則是未指定值(undefined)。
  • Java 虛擬機棧(Java Virtual Machine Stack),早期也叫 Java 棧。每一個線程在建立時都會建立一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次的 Java 方法調用。前面談程序計數器時,提到了當前方法;同理,在一個時間點,對應的只會有一個活動的棧幀,一般叫做當前幀,方法所在的類叫做當前類。若是在該方法中調用了其餘方法,對應的新的棧幀會被建立出來,成爲新的當前幀,一直到它返回結果或者執行結束。JVM 直接對 Java 棧的操做只有兩個,就是對棧幀的壓棧和出棧。棧幀中存儲着局部變量表、操做數(operand)棧、動態連接、方法正常退出或者異常退出的定義等。
  • 堆(Heap),它是 Java 內存管理的核心區域,用來放置 Java 對象實例,幾乎全部建立的Java 對象實例都是被直接分配在堆上。堆被全部的線程共享,在虛擬機啓動時,咱們指定的「Xmx」之類參數就是用來指定最大堆空間等指標。理所固然,堆也是垃圾收集器重點照顧的區域,因此堆內空間還會被不同的垃圾收集器進行進一步的細分,最有名的就是新生代、老年代的劃分。
  • 方法區(Method Area)。這也是全部線程共享的一塊內存區域,用於存儲所謂的元(Meta)數據,例如類結構信息,以及對應的運行時常量池、字段、方法代碼等。因爲早期的 Hotspot JVM 實現,不少人習慣於將方法區稱爲永久代(Permanent Generation)。Oracle JDK 8 中將永久代移除,同時增長了元數據區(Metaspace)。
  • 運行時常量池(Run-Time Constant Pool),這是方法區的一部分。若是仔細分析過反編譯的類文件結構,你能看到版本號、字段、方法、超類、接口等各類信息,還有一項信息就是常量池。Java 的常量池能夠存放各類常量信息,不管是編譯期生成的各類字面量,仍是須要在運行時決定的符號引用,因此它比通常語言的符號表存儲的信息更加寬泛。
  • 本地方法棧(Native Method Stack)。它和 Java 虛擬機棧是很是類似的,支持對本地方法的調用,也是每一個線程都會建立一個。在 Oracle Hotspot JVM 中,本地方法棧和 Java 虛擬機棧是在同一起區域,這徹底取決於技術實現的決定,並未在規範中強制。

形成OOM的緣由有哪幾種?

  • 堆內存不足是最多見的 OOM 緣由之一,拋出的錯誤信息是「java.lang.OutOfMemoryError:Java heap space」,緣由可能千奇百怪,例如,可能存在內存泄漏問題;也頗有可能就是堆的大小不合理,好比咱們要處理比較可觀的數據量,可是沒有顯式指定 JVM 堆大小或者指定數值偏小;或者出現 JVM 處理引用不及時,致使堆積起來,內存沒法釋放等。
  • 虛擬機棧和本地方法棧,這里要稍微複雜一點。若是咱們寫一段程序不斷的進行遞歸調用,並且沒有退出條件,就會致使不斷地進行壓棧。相似這種狀況,JVM 實際會拋出StackOverFlowError;固然,若是 JVM 試圖去擴展棧空間的的時候失敗,則會拋出OutOfMemoryError。
  • 對於老版本的 Oracle JDK,由於永久代的大小是有限的,而且 JVM 對永久代垃圾回收(如,常量池回收、卸載不再須要的類型)很是不積極,因此當咱們不斷添加新類型的時候,永久代出現OutOfMemoryError 也很是多見,尤爲是在運行時存在大量動態類型生成的場合;相似 Intern 字符串緩存佔用太多空間,也會致使 OOM 問題。對應的異常信息,會標記出來和永久代相關:「java.lang.OutOfMemoryError: PermGenspace

GC 算法

  • 複製(Copying)算法,我前面講到的新生代 GC,基本都是基於複製算法,將活着的對象複製到 to 區域,拷貝過程當中將對象順序放置,就能夠避免內存碎片化。這麼作的代價是,既然要進行複製,既要提早預留內存空間,有必定的浪費;另外,對於 G1 這種分拆成爲大量 region 的 GC,複製而不是移動,意味着 GC 須要維護 region 之間對象引用關係,這個開銷也不小,不管是內存佔用或者時間開銷。
  • 標記 - 清除(Mark-Sweep)算法,首先進行標記工做,標識出全部要回收的對象,而後進行清除。這麼作除了標記、清除過程效率有限,另外就是不可避免的出現碎片化問題,這就致使其不適合特別大的堆;不然,一旦出現 Full GC,暫停時間可能根本沒法接受。
  • 標記 - 整理(Mark-Compact),相似於標記 - 清除,但爲避免內存碎片化,它會在清理過程當中將對象移動,以確保移動後的對象佔用連續的內存空間。

G1 垃圾回收器採用的是什麼垃圾回收算法?

從 GC 算法的角度,G1 選擇的是複合算法,能夠簡化理解爲:java

  • 在新生代,G1 採用的仍然是並行的複製算法,因此一樣會發生 Stop-The-World 的暫停。
  • 在老年代,大部分狀況下都是併發標記,而整理(Compact)則是和新生代 GC 時捎帶進行,而且不是總體性的整理,而是增量進行的。

GC 調優思路

從性能的角度看,一般關注三個方面,內存佔用(footprint)、延時(latency)和吞吐量(throughput),大多數狀況下調優會側重於其中一個或者兩個方面的目標,不多有狀況能夠兼顧三個不同的角度。固然,除了上面一般的三個方面,也可能須要考慮其餘 GC 相關的場景,例如,OOM 也可能與不合理的 GC 相關參數有關;或者,應用啓動速度方面的需求,GC 也會是個考慮的方面。 基本的調優思路能夠總結爲:git

  • 理解應用需求和問題,肯定調優目標。假設,咱們開發了一個應用服務,但發現偶爾會出現性能抖動,出現較長的服務停頓。評估用戶可接受的響應時間和業務量,將目標簡化爲,但願 GC 暫停盡量控制在 200ms 之內,而且保證必定標準的吞吐量。
  • 掌握 JVM 和 GC 的狀態,定位具體的問題,肯定真的有 GC 調優的必要。具體有不少方法,好比,經過 jstat 等工具查看 GC 等相關狀態,能夠開啓 GC 日誌,或者是利用操做系統提供的診斷工具等。例如,經過追蹤 GC 日誌,就能夠查找是不是 GC 在特定時間發生了長時間的暫停,進而致使了應用響應不及時。
  • 選擇的 GC 類型是否符合咱們的應用特徵,若是是,具體問題表如今哪里,是 Minor GC 過長,仍是 Mixed GC 等出現異常停頓狀況;若是不是,考慮切換到什麼類型,如 CMS 和 G1 都是更側重於低延遲的 GC 選項。

經過分析肯定具體調整的參數或者軟硬件配置。驗證是否達到調優目標,若是達到目標,便可以考慮結束調優;不然,重複完成分析、調整、驗證這 個過程。程序員

如何提升JVM的性能?

  1. 新對象預留在年輕代 經過設置一個較大的年輕代預留新對象,設置合理的 Survivor 區而且提供 Survivor 區的使用率,能夠將年輕對象保存在年輕代。github

  2. 大對象進入年老代 使用參數-XX:PetenureSizeThreshold 設置大對象直接進入年老代的閾值算法

  3. 設置對象進入年老代的年齡 這個閾值的最大值能夠經過參數-XX:MaxTenuringThreshold 來設置,默認值是 15spring

  4. 穩定的 Java 堆 得到一個穩定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 同樣。編程

  5. 增大吞吐量提高系統性能 –Xmx380m –Xms3800m:設置 Java 堆的最大值和初始值。通常狀況下,爲了不堆內存的頻繁震盪,致使系統性能降低,咱們的作法是設置最大堆等於最小堆。假設這裏把最小堆減小爲最大堆的一半,即 1900m,那麼 JVM 會盡量在 1900MB 堆空間中運行,若是這樣,發生 GC 的可能性就會比較高; -Xss128k:減小線程棧的大小,這樣可使剩餘的系統內存支持更多的線程; -Xmn2g:設置年輕代區域大小爲 2GB; –XX:+UseParallelGC:年輕代使用並行垃圾回收收集器。這是一個關注吞吐量的收集器,能夠儘量地減小 GC 時間。 –XX:ParallelGC-Threads:設置用於垃圾回收的線程數,一般狀況下,能夠設置和 CPU 數量相等。但在 CPU 數量比較多的狀況下,設置相對較小的數值也是合理的; –XX:+UseParallelOldGC:設置年老代使用並行回收收集器。緩存

  6. 嘗試使用大的內存分頁 –XX:+LargePageSizeInBytes:設置大頁的大小。 內存分頁 (Paging) 是在使用 MMU 的基礎上,提出的一種內存管理機制。它將虛擬地址和物理地址按固定大小(4K)分割成頁 (page) 和頁幀 (page frame),並保證頁與頁幀的大小相同。這種機制,從數據結構上,保證了訪問內存的高效,並使 OS 能支持非連續性的內存分配。安全

  7. 使用非佔有的垃圾回收器 爲下降應用軟件的垃圾回收時的停頓,首先考慮的是使用關注系統停頓的 CMS 回收器,其次,爲了減小 Full GC 次數,應儘量將對象預留在年輕代。bash

system.gc() 的做用是什麼?

gc()函數的做用只是提醒虛擬機:程序員但願進行一次垃圾回收。可是它不能保證垃圾回收必定會進行,並且具體何時進行是取決於具體的虛擬機的,不一樣的虛擬機有不一樣的對策。

Parallel GC、CMS GC、ZGC、Azul Pauseless GC最主要的不一樣是?背後的原理也請簡單描述下?

Parallel GC的Young區採用的是Mark-Copy算法,Old區採用的是Mark-Sweep-Compact來實現,Parallel執行,因此決定了Parallel GC在執行YGC、FGC時都會Stop-The-World,但完成GC的速度也會比較快。 CMS GC的Young區採用的也是Mark-Copy,Old區採用的是Concurrent Mark-Sweep,因此決定了CMS GC在對old區回收時形成的STW時間會更短,避免對應用產生太大的時延影響。 G1 GC採用了Garbage First算法,比較複雜,實現的好呢,理論上是會比CMS GC能夠更高效,同時對應用的影響也很小。 ZGC、Azul Pauseless GC採用的算法很不同,尤爲是Pauseless GC,其中的很重要的一個技巧是經過增長Read Barrier來更好的識別對GC而言最關鍵的references變化的狀況。

何時執行ygc,fullgc?

當young gen中的eden區分配滿的時候觸發young gc,當年老代內存不足時,將執行Major GC,也叫 Full GC。

gc()函數的做用只是提醒虛擬機:程序員但願進行一次垃圾回收。可是它不能保證垃圾回收必定會進行,並且具體何時進行是取決於具體的虛擬機的,不一樣的虛擬機有不一樣的對策。

強引用、軟引用、弱引用、幻象引用有什麼區別?具體使用場景是什麼?

不一樣的引用類型,主要體現的是對象不一樣的可達性(reachable)狀態和對垃圾收集的影響。

所謂強引用("Strong" Reference),就是咱們最多見的普通對象引用,只要還有強引用指向一個對象,就能代表對象還「活着」,垃圾收集器不會碰這種對象。對於一個普通的對象,若是沒有其餘的引用關係,只要超過了引用的做用域或者顯式地將相應(強)引用賦值爲 null,就是能夠被垃圾收集的了,固然具體回收時機仍是要看垃圾收集策略。

軟引用(SoftReference),是一種相對強引用弱化一些的引用,可讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出OutOfMemoryError 以前,清理軟引用指向的對象。軟引用一般用來實現內存敏感的緩存,若是還有空閒內存,就能夠暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。

SoftReference 在「弱引用WeakReference」中屬於最強的引用。SoftReference 所指向的對象,當沒有強引用指向它時,會在內存中停留一段的時間,垃圾回收器會根據 JVM 內存的使用狀況(內存的緊缺程度)以及 SoftReference 的 get() 方法的調用狀況來決定是否對其進行回收。

對於幻象引用(PhantomReference ),有時候也翻譯成虛引用,你不能經過它訪問對象。幻象引用僅僅是提供了一種確保對象被 finalize 之後,作某些事情的機制,好比,一般用來作所謂的 Post-Mortem 清理機制,如 Java 平臺自身 Cleaner 機制等,也有人利用幻象引用監控對象的建立和銷燬。

Object counter = new Object();
ReferenceQueue refQueue = new ReferenceQueue<>();
PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
counter = null;
System.gc();
try {
    // Remove 是一個阻塞方法,能夠指定 timeout,或者選擇一直阻塞
    Reference<Object> ref = refQueue.remove(1000L);
    if (ref != null) {
        // do something
    }
} catch (InterruptedException e) {
    // Handle it
}
複製代碼

JVM類加載過程

通常來講,咱們把 Java 的類加載過程分爲三個主要步驟:加載、連接、初始化。 首先是加載階段(Loading),它是 Java 將字節碼數據從不同的數據源讀取到 JVM 中,並映射爲 JVM 承認的數據結構(Class 對象),這里的數據源多是各類各樣的形態,如 jar 文件、class 文件,甚至是網絡數據源等;若是輸入數據不是 ClassFile 的結構,則會拋出 ClassFormatError。加載階段是用戶參與的階段,咱們能夠自定義類加載器,去實現本身的類加載過程。

第二階段是連接(Linking),這是核心的步驟,簡單說是把原始的類定義信息平滑地轉化入 JVM 運行的過程當中。這里可進一步細分爲三個步驟:

  • 驗證(Verification),這是虛擬機安全的重要保障,JVM 須要覈驗字節信息是符合 Java 虛擬機規範的,不然就被認爲是 VerifyError,這樣就防止了惡意信息或者不合規的信息危害 JVM 的運行,驗證階段有可能觸發更多 class 的加載。
  • 準備(Preparation),建立類或接口中的靜態變量,並初始化靜態變量的初始值。但這里的「初始化」和下面的顯式初始化階段是有區別的,側重點在於分配所須要的內存空間,不會去執行更進一步的 JVM 指令。
  • 解析(Resolution),在這一步會將常量池中的符號引用(symbolic reference)替換爲直接引用。

最後是初始化階段(initialization),這一步真正去執行類初始化的代碼邏輯,包括靜態字段賦值的動做,以及執行類定義中的靜態初始化塊內的邏輯,編譯器在編譯階段就會把這部分邏輯整理好,父類型的初始化邏輯優先於當前類型的邏輯。

什麼是雙親委派模型?

簡單說就是當類加載器(Class-Loader)試圖加載某個類型的時候,除非父加載器找不到相應類型,不然盡量將這個任務代理給當前加載器的父加載器去作。使用委派模型的目的是避免重複加載 Java 類型。

類加載器的類型

  • 啓動類加載器(Bootstrap Class-Loader),加載 jre/lib 下面的 jar 文件,如 rt.jar。它是個超級公民,即便是在開啓了 Security Manager 的時候,JDK 仍賦予了它加載的程序 AllPermission。
  • 擴展類加載器(Extension or Ext Class-Loader),負責加載咱們放到 jre/lib/ext/ 目錄下面的 jar 包,這就是所謂的 extension 機制。該目錄也能夠經過設置 「java.ext.dirs」來覆蓋。
  • 應用類加載器(Application or App Class-Loader),就是加載咱們最熟悉的 classpath

上下文類加載器

Java 提供了不少服務提供者接口(Service Provider Interface,SPI),容許第三方爲這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,而這些 SPI 的實現代碼則是做爲 Java 應用所依賴的 jar 包被包含進類路徑(CLASSPATH)裏。SPI接口中的代碼常常須要加載具體的實現類。那麼問題來了,SPI的接口是Java核心庫的一部分,是由**啓動類加載器(Bootstrap Classloader)來加載的;SPI的實現類是由系統類加載器(System ClassLoader)**來加載的。引導類加載器是沒法找到 SPI 的實現類的,由於依照雙親委派模型,BootstrapClassloader沒法委派AppClassLoader來加載類。而線程上下文類加載器破壞了「雙親委派模型」,能夠在執行線程中拋棄雙親委派加載鏈模式,使程序能夠逆向使用類加載器。

ServiceLoader 的加載代碼:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
複製代碼

ContextClassLoader默認存放了AppClassLoader的引用,因爲它是在運行時被放在了線程中,因此無論當前程序處於何處(BootstrapClassLoader或是ExtClassLoader等),在任何須要的時候均可以用Thread.currentThread().getContextClassLoader()取出應用程序類加載器來完成須要的操做。

自定義類加載器

自定義類加載器,常見的場景有:

  • 實現相似進程內隔離,類加載器實際上用做不同的命名空間,以提供相似容器、模塊化的效果。例如,兩個模塊依賴於某個類庫的不同版本,若是分別被不同的容器加載,就能夠互不干擾。這個方面的集大成者是Java EE和OSGI、JPMS等框架。
  • 應用須要從不同的數據源獲取類定義信息,例如網絡數據源,而不是本地文件系統。
  • 須要本身操縱字節碼,動態修改或者生成類型

從本地路徑 load class 的例子:

public class CustomClassLoader extends ClassLoader {
 
    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassFromFile(name);
        return defineClass(name, b, 0, b.length);
    }
 
    private byte[] loadClassFromFile(String fileName)  {
        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
                fileName.replace('.', File.separatorChar) + ".class");
        byte[] buffer;
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        int nextValue = 0;
        try {
            while ( (nextValue = inputStream.read()) != -1 ) {
                byteStream.write(nextValue);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        buffer = byteStream.toByteArray();
        return buffer;
    }
}
複製代碼

動態代理的原理?

反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自省(introspect,官方用語)的能力。經過反射咱們能夠直接操做類或者對象,好比獲取某個對象的類定義,獲取類聲明的屬性和方法,調用方法或者構造對象,甚至能夠運行時修改類定義。 動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制,不少場景都是利用相似機制作到的,好比用來包裝 RPC 調用、面向切面的編程(AOP)。 實現動態代理的方式不少,好比 JDK 自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其餘的實現方式,好比利用傳說中更高性能的字節碼操做機制,相似 ASM、cglib(基於 ASM)、Javassist 等。

如何使用JDK動態代理?

public class MyDynamicProxy {
    public static  void main (String[] args) {
        HelloImpl hello = new HelloImpl();
        MyInvocationHandler handler = new MyInvocationHandler(hello);
        // 構造代碼實例
        Hello proxyHello = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler);
        // 調用代理方法
        proxyHello.sayHello();
    }
}
interface Hello {
    void sayHello();
}
class HelloImpl implements  Hello {
    @Override
    public void sayHello() {
        System.out.println("Hello World");
    }
}
class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        System.out.println("Invoking sayHello");
        Object result = method.invoke(target, args);
        return result;
    }
}
複製代碼

動態代理:JDK動態代理和CGLIB代理的區別?

JDK動態代理只能對實現了接口的類生成代理,而不能針對類,CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法(繼承)。

JDK Proxy 的優點:

  1. 最小化依賴關係,減小依賴意味着簡化開發和維護,JDK 自己的支持,可能比 cglib 更加可靠。
  2. 平滑進行 JDK 版本升級,而字節碼類庫一般須要進行更新以保證在新版 Java 上可以使用。
  3. 代碼實現簡單。

基於相似 cglib 框架的優點:

  1. 有的時候調用目標可能不便實現額外接口,從某種角度看,限定調用者實現接口是有些侵入性的實踐,相似 cglib 動態代理就沒有這種限制。
  2. 只操做咱們關心的類,而沒必要爲其餘相關類增長工做量。
  3. 高性能。

Spring在選擇用JDK仍是CGLiB的依據是什麼?

(1)當Bean實現接口時,Spring就會用JDK的動態代理 (2)當Bean沒有實現接口時,Spring使用CGlib是實現 (3)能夠強制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)

CGlib比JDK快?

(1)使用CGLib實現動態代理,CGLib底層採用ASM字節碼生成框架,使用字節碼技術生成代理類,比使用Java反射效率要高。惟一須要注意的是,CGLib不能對聲明爲final的方法進行代理,由於CGLib原理是動態生成被代理類的子類。可是JDK也在升級,開始引入不少字節碼技術來實現部分動態代理的功能,因此在某些測試下不必定是CGLib更快。

Java 中操做字節碼的技術

ASM、Javassist、CGLib、Byte Budy。

相關文章
相關標籤/搜索