外貿領航
首頁行業資訊 > jvm面試問題「快穿之女配虐渣日常」

jvm面試問題「快穿之女配虐渣日常」

來源:互聯網 2024-08-19 13:04:01

原文鏈接:https://mp.weixin.qq.com/s/XYsEJyIO46jXhHE1sOR_0Q

1.什么是JVM?

JVM——Java虛擬機,它是Java實現平臺無關性的基石。

Java程序運行的時候,編譯器將Java文件編譯成平臺無關的Java字節碼文件(.class),接下來對應平臺JVM對字節碼文件進行解釋,翻譯成對應平臺匹配的機器指令并運行。

Java語言編譯運行

同時JVM也是一個跨語言的平臺,和語言無關,只和class的文件格式關聯,任何語言,只要能翻譯成符合規范的字節碼文件,都能被JVM運行。

JVM跨語言

內存管理2.能說一下JVM的內存區域嗎?

JVM內存區域最粗略的劃分可以分為堆和棧,當然,按照虛擬機規范,可以劃分為以下幾個區域:

Java虛擬機運行時數據區

JVM內存分為線程私有區和線程共享區,其中方法區和堆是線程共享區,虛擬機棧、本地方法棧和程序計數器是線程隔離的數據區。

1、程序計數器

程序計數器(Program Counter Register)也被稱為PC寄存器,是一塊較小的內存空間。

它可以看作是當前線程所執行的字節碼的行號指示器。

2、Java虛擬機棧

Java虛擬機棧(Java Virtual Machine Stack)也是線程私有的,它的生命周期與線程相同。

Java虛擬機棧描述的是Java方法執行的線程內存模型:方法執行時,JVM會同步創建一個棧幀,用來存儲局部變量表、操作數棧、動態連接等。

Java虛擬機棧

3、本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別只是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的本地(Native)方法服務。

Java 虛擬機規范允許本地方法棧被實現成固定大小的或者是根據計算動態擴展和收縮的。

4、Java堆

對于Java應用程序來說,Java堆(Java Heap)是虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,Java里“幾乎”所有的對象實例都在這里分配內存。

Java堆是垃圾收集器管理的內存區域,因此一些資料中它也被稱作“GC堆”(Garbage Collected Heap,)。從回收內存的角度看,由于現代垃圾收集器大部分都是基于分代收集理論設計的,所以Java堆中經常會出現新生代、老年代、Eden空間、From Survivor空間、To Survivor空間等名詞,需要注意的是這種劃分只是根據垃圾回收機制來進行的劃分,不是Java虛擬機規范本身制定的。

Java 堆內存結構

5.方法區

方法區是比較特別的一塊區域,和堆類似,它也是各個線程共享的內存區域,用于存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等數據。

它特別在Java虛擬機規范對它的約束非常寬松,所以方法區的具體實現歷經了許多變遷,例如jdk1.7之前使用永久代作為方法區的實現。

3.說一下JDK1.6、1.7、1.8內存區域的變化?

JDK1.6、1.7/1.8內存區域發生了變化,主要體現在方法區的實現:

JDK1.6使用永久代實現方法區:

JDK 1.6內存區域

JDK1.7時發生了一些變化,將字符串常量池、靜態變量,存放在堆上

JDK 1.7內存區域

在JDK1.8時徹底干掉了永久代,而在直接內存中劃出一塊區域作為元空間,運行時常量池、類常量池都移動到元空間。JDK 1.8內存區域4.為什么使用元空間替代永久代作為方法區的實現?

Java虛擬機規范規定的方法區只是換種方式實現。有客觀和主觀兩個原因。

客觀上使用永久代來實現方法區的決定的設計導致了Java應用更容易遇到內存溢出的問題(永久代有-XX:MaxPermSize的上限,即使不設置也有默認大小,而J9和JRockit只要沒有觸碰到進程可用內存的上限,例如32位系統中的4GB限制,就不會出問題),而且有極少數方法 (例如String::intern())會因永久代的原因而導致不同虛擬機下有不同的表現。主觀上當Oracle收購BEA獲得了JRockit的所有權后,準備把JRockit中的優秀功能,譬如Java Mission Control管理工具,移植到HotSpot 虛擬機時,但因為兩者對方法區實現的差異而面臨諸多困難。考慮到HotSpot未來的發展,在JDK 6的 時候HotSpot開發團隊就有放棄永久代,逐步改為采用本地內存(Native Memory)來實現方法區的計劃了,到了JDK 7的HotSpot,已經把原本放在永久代的字符串常量池、靜態變量等移出,而到了 JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內存中實現的元空間(Meta-space)來代替,把JDK 7中永久代還剩余的內容(主要是類型信息)全部移到元空間中。5.對象創建的過程了解嗎?

在JVM中對象的創建,我們從一個new指令開始:

首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,就先執行相應的類加載過程類加載檢查通過后,接下來虛擬機將為新生對象分配內存。內存分配完成之后,虛擬機將分配到的內存空間(但不包括對象頭)都初始化為零值。接下來設置對象頭,請求頭里包含了對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。

這個過程大概圖示如下:

對象創建過程

6.什么是指針碰撞?什么是空閑列表?

內存分配有兩種方式,指針碰撞(Bump The Pointer)、空閑列表(Free List)。

指針碰撞和空閑列表

指針碰撞:假設Java堆中內存是絕對規整的,所有被使用過的內存都被放在一邊,空閑的內存被放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”。空閑列表:如果Java堆中的內存并不是規整的,已被使用的內存和空閑的內存相互交錯在一起,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”。兩種方式的選擇由Java堆是否規整決定,Java堆是否規整是由選擇的垃圾收集器是否具有壓縮整理能力決定的。7.JVM 里 new 對象時,堆會發生搶占嗎?JVM是怎么設計來保證線程安全的?

會,假設JVM虛擬機上,每一次new 對象時,指針就會向右移動一個對象size的距離,一個線程正在給A對象分配內存,指針還沒有來的及修改,另一個為B對象分配內存的線程,又引用了這個指針來分配內存,這就發生了搶占。

有兩種可選方案來解決這個問題:

堆搶占和解決方案

采用CAS分配重試的方式來保證更新操作的原子性每個線程在Java堆中預先分配一小塊內存,也就是本地線程分配緩沖(Thread Local AllocationBuffer,TLAB),要分配內存的線程,先在本地緩沖區中分配,只有本地緩沖區用完了,分配新的緩存區時才需要同步鎖定。8.能說一下對象的內存布局嗎?

在HotSpot虛擬機里,對象在堆內存中的存儲布局可以劃分為三個部分:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

對象的存儲布局

對象頭主要由兩部分組成:

第一部分存儲對象自身的運行時數據:哈希碼、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等,官方稱它為Mark Word,它是個動態的結構,隨著對象狀態變化。第二部分是類型指針,指向對象的類元數據類型(即對象代表哪個類)。此外,如果對象是一個Java數組,那還應該有一塊用于記錄數組長度的數據

實例數據用來存儲對象真正的有效信息,也就是我們在程序代碼里所定義的各種類型的字段內容,無論是從父類繼承的,還是自己定義的。

對齊填充不是必須的,沒有特別含義,僅僅起著占位符的作用。

9.對象怎么訪問定位?

Java程序會通過棧上的reference數據來操作堆上的具體對象。由于reference類型在《Java虛擬機規范》里面只規定了它是一個指向對象的引用,并沒有定義這個引用應該通過什么方式去定位、訪問到堆中對象的具體位置,所以對象訪問方式也是由虛擬機實現而定的,主流的訪問方式主要有使用句柄和直接指針兩種:

如果使用句柄訪問的話,Java堆中將可能會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息,其結構如圖所示:

通過句柄訪問對象

如果使用直接指針訪問的話,Java堆中對象的內存布局就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接訪問的開銷,如圖所示:

通過直接指針訪問對象

這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要被修改。

使用直接指針來訪問最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象訪問在Java中非常頻繁,因此這類開銷積少成多也是一項極為可觀的執行成本。

HotSpot虛擬機主要使用直接指針來進行對象訪問。

10.內存溢出和內存泄漏是什么意思?

內存泄露就是申請的內存空間沒有被正確釋放,導致內存被白白占用。

內存溢出就是申請的內存超過了可用內存,內存不夠了。

兩者關系:內存泄露可能會導致內存溢出。

用一個有味道的比喻,內存溢出就是排隊去蹲坑,發現沒坑位了,內存泄漏,就是有人占著茅坑不拉屎,占著茅坑不拉屎的多了可能會導致坑位不夠用。

內存泄漏、內存溢出

11.能手寫內存溢出的例子嗎?

在JVM的幾個內存區域中,除了程序計數器外,其他幾個運行時區域都有發生內存溢出(OOM)異常的可能,重點關注堆和棧。

Java堆溢出

Java堆用于儲存對象實例,只要不斷創建不可被回收的對象,比如靜態對象,那么隨著對象數量的增加,總容量觸及最大堆的容量限制后就會產生內存溢出異常(OutOfMemoryError)。

這就相當于一個房子里,不斷堆積不能被收走的雜物,那么房子很快就會被堆滿了。

/** * VM參數: -Xms20m -Xmx20m -XX: HeapDumpOnOutOfMemoryError */public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } }}虛擬機棧.OutOfMemoryError

JDK使用的HotSpot虛擬機的棧內存大小是固定的,我們可以把棧的內存設大一點,然后不斷地去創建線程,因為操作系統給每個進程分配的內存是有限的,所以到最后,也會發生OutOfMemoryError異常。

/** * vm參數:-Xss2M */public class JavaVMStackOOM { private void dontStop() { while (true) { } } public void stackLeakByThread() { while (true) { Thread thread = new Thread(new Runnable() { public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) throws Throwable { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); }}12.內存泄漏可能由哪些原因導致呢?

內存泄漏可能的原因有很多種:

內存泄漏可能原因

靜態集合類引起內存泄漏

靜態集合的生命周期和 JVM 一致,所以靜態集合引用的對象不能被釋放。

public class OOM { static List list = new ArrayList(); public void oomTests(){ Object obj = new Object(); list.add(obj); }}

單例模式

和上面的例子原理類似,單例對象在初始化后會以靜態變量的方式在 JVM 的整個生命周期中存在。如果單例對象持有外部的引用,那么這個外部對象將不能被 GC 回收,導致內存泄漏。

數據連接、IO、Socket等連接

創建的連接不再使用時,需要調用 close 方法關閉連接,只有連接被關閉后,GC 才會回收對應的對象(Connection,Statement,ResultSet,Session)。忘記關閉這些資源會導致持續占有內存,無法被 GC 回收。

try { Connection conn = null; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("url", "", ""); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("...."); } catch (Exception e) { }finally { //不關閉連接 } }

變量不合理的作用域

一個變量的定義作用域大于其使用范圍,很可能存在內存泄漏;或不再使用對象沒有及時將對象設置為 null,很可能導致內存泄漏的發生。

public class Simple { Object object; public void method1(){ object = new Object(); //...其他代碼 //由于作用域原因,method1執行完成之后,object 對象所分配的內存不會馬上釋放 object = null; }}

hash值發生變化

對象Hash值改變,使用HashMap、HashSet等容器中時候,由于對象修改之后的Hah值和存儲進容器時的Hash值不同,所以無法找到存入的對象,自然也無法單獨刪除了,這也會造成內存泄漏。說句題外話,這也是為什么String類型被設置成了不可變類型。

ThreadLocal使用不當

ThreadLocal的弱引用導致內存泄漏也是個老生常談的話題了,使用完ThreadLocal一定要記得使用remove方法來進行清除。

13.如何判斷對象仍然存活?

有兩種方式,**引用計數算法(reference counting)**和可達性分析算法。

引用計數算法

引用計數器的算法是這樣的:在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的。

引用計數算法

可達性分析算法

目前 Java 虛擬機的主流垃圾回收器采取的是可達性分析算法。這個算法的實質在于將一系列 GC Roots 作為初始的存活對象合集(Gc Root Set),然后從該合集出發,探索所有能夠被該集合引用到的對象,并將其加入到該集合中,這個過程我們也稱之為標記(mark)。最終,未被探索到的對象便是死亡的,是可以回收的。

14.Java中可作為GC Roots的對象有哪幾種?

可以作為GC Roots的主要有四種對象:

虛擬機棧(棧幀中的本地變量表)中引用的對象方法區中類靜態屬性引用的對象方法區中常量引用的對象本地方法棧中JNI引用的對象15.說一下對象有哪幾種引用?

Java中的引用有四種,分為強引用(Strongly Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

強引用是最傳統的引用的定義,是指在程序代碼之中普遍存在的引用賦值,無論任何情況下,只要強引用關系還存在,垃圾收集器就永遠不會回收掉被引用的對象。Object obj =new Object();軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關聯著的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內存, 才會拋出內存溢出異常。在JDK 1.2版之后提供了SoftReference類來實現軟引用。 Object obj = new Object(); ReferenceQueue queue = new ReferenceQueue(); SoftReference reference = new SoftReference(obj, queue); //強引用對象滯空,保留軟引用 obj = null;弱引用也是用來描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2版之后提供了WeakReference類來實現弱引用。 Object obj = new Object(); ReferenceQueue queue = new ReferenceQueue(); WeakReference reference = new WeakReference(obj, queue); //強引用對象滯空,保留軟引用 obj = null;虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2版之后提供了PhantomReference類來實現虛引用。 Object obj = new Object(); ReferenceQueue queue = new ReferenceQueue(); PhantomReference reference = new PhantomReference(obj, queue); //強引用對象滯空,保留軟引用 obj = null;

四種引用總結

16.finalize()方法了解嗎?有什么作用?

用一個不太貼切的比喻,垃圾回收就是古代的秋后問斬,finalize()就是刀下留人,在人犯被處決之前,還要做最后一次審計,青天大老爺看看有沒有什么冤情,需不需要刀下留人。

刀下留人

如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。如果對象在在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己 (this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它就”逃過一劫“;但是如果沒有抓住這個機會,那么對象就真的要被回收了。

17.Java堆的內存分區了解嗎?

按照垃圾收集,將Java堆劃分為**新生代 (Young Generation)和老年代(Old Generation)**兩個區域,新生代存放存活時間短的對象,而每次回收后存活的少量對象,將會逐步晉升到老年代中存放。

而新生代又可以分為三個區域,eden、from、to,比例是8:1:1,而新生代的內存分區同樣是從垃圾收集的角度來分配的。

Java堆內存劃分

18.垃圾收集算法了解嗎?

垃圾收集算法主要有三種:

標記-清除算法

見名知義,標記-清除(Mark-Sweep)算法分為兩個階段:

標記 : 標記出所有需要回收的對象清除:回收所有被標記的對象

標記-清除算法

標記-清除算法比較基礎,但是主要存在兩個缺點:

執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執行效率都隨對象數量增長而降低。內存空間的碎片化問題,標記、清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致當以后在程序運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。標記-復制算法

標記-復制算法解決了標記-清除算法面對大量可回收對象時執行效率低的問題。

過程也比較簡單:將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。

標記-復制算法

這種算法存在一個明顯的缺點:一部分空間沒有使用,存在空間的浪費。

新生代垃圾收集主要采用這種算法,因為新生代的存活對象比較少,每次復制的只是少量的存活對象。當然,實際新生代的收集不是按照這個比例。

標記-整理算法

為了降低內存的消耗,引入一種針對性的算法:標記-整理(Mark-Compact)算法。

其中的標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然后直接清理掉邊界以外的內存。

標記-整理算法

標記-整理算法主要用于老年代,移動存活對象是個極為負重的操作,而且這種操作需要Stop The World才能進行,只是從整體的吞吐量來考量,老年代使用標記-整理算法更加合適。

19.說一下新生代的區域劃分?

新生代的垃圾收集主要采用標記-復制算法,因為新生代的存活對象比較少,每次復制少量的存活對象效率比較高。

基于這種算法,虛擬機將內存分為一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。發生垃圾收集時,將Eden和Survivor中仍然存活的對象一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間。默認Eden和Survivor的大小比例是8∶1。

新生代內存劃分

20.Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC都是什么意思?

部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,其中又分為:

新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。

整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。

21.Minor GC/Young GC什么時候觸發?

新創建的對象優先在新生代Eden區進行分配,如果Eden區沒有足夠的空間時,就會觸發Young GC來清理新生代。

22.什么時候會觸發Full GC?

這個觸發條件稍微有點多,往下看:

Full GC觸發條件

Young GC之前檢查老年代:在要進行 Young GC 的時候,發現老年代可用的連續內存空間 < 新生代歷次Young GC后升入老年代的對象總和的平均大小,說明本次Young GC后可能升入老年代的對象大小,可能超過了老年代當前可用內存空間,那就會觸發 Full GC。Young GC之后老年代空間不足:執行Young GC之后有一批對象需要放入老年代,此時老年代就是沒有足夠的內存空間存放這些對象了,此時必須立即觸發一次Full GC老年代空間不足,老年代內存使用率過高,達到一定比例,也會觸發Full GC。空間分配擔保失敗( Promotion Failure),新生代的 To 區放不下從 Eden 和 From 拷貝過來對象,或者新生代對象 GC 年齡到達閾值需要晉升這兩種情況,老年代如果放不下的話都會觸發 Full GC。方法區內存空間不足:如果方法區由永久代實現,永久代空間不足 Full GC。System.gc()等命令觸發:System.gc()、jmap -dump 等命令會觸發 full gc。23.對象什么時候會進入老年代?

對象進入老年代

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

在對象的對象頭信息中存儲著對象的迭代年齡,迭代年齡會在每次YoungGC之后對象的移區操作中增加,每一次移區年齡加一.當這個年齡達到15(默認)之后,這個對象將會被移入老年代。

可以通過這個參數設置這個年齡值。

- XX:MaxTenuringThreshold

大對象直接進入老年代

有一些占用大量連續內存空間的對象在被加載就會直接進入老年代.這樣的大對象一般是一些數組,長字符串之類的對。

HotSpot虛擬機提供了這個參數來設置。

-XX:PretenureSizeThreshold

動態對象年齡判定

為了能更好地適應不同程序的內存狀況,HotSpot虛擬機并不是永遠要求對象的年齡必須達到- XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代。

空間分配擔保

假如在Young GC之后,新生代仍然有大量對象存活,就需要老年代進行分配擔保,把Survivor無法容納的對象直接送入老年代。

24.知道有哪些垃圾收集器嗎?

主要垃圾收集器如下,圖中標出了它們的工作區域、垃圾收集算法,以及配合關系。

HotSpot虛擬機垃圾收集器

這些收集器里,面試的重點是兩個——CMS和G1。

Serial收集器

Serial收集器是最基礎、歷史最悠久的收集器。

如同它的名字(串行),它是一個單線程工作的收集器,使用一個處理器或一條收集線程去完成垃圾收集工作。并且進行垃圾收集時,必須暫停其他所有工作線程,直到垃圾收集結束——這就是所謂的“Stop The World”。

Serial/Serial Old收集器的運行過程如圖:

Serial/Serial Old收集器運行示意圖

ParNew

ParNew收集器實質上是Serial收集器的多線程并行版本,使用多條線程進行垃圾收集。

ParNew/Serial Old收集器運行示意圖如下:

ParNew/Serial Old收集器運行示意圖

Parallel Scavenge

Parallel Scavenge收集器是一款新生代收集器,基于標記-復制算法實現,也能夠并行收集。和ParNew有些類似,但Parallel Scavenge主要關注的是垃圾收集的吞吐量——所謂吞吐量,就是CPU用于運行用戶代碼的時間和總消耗時間的比值,比值越大,說明垃圾收集的占比越小。

吞吐量

Serial Old

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法。

Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程并發收集,基于標記-整理算法實現。

Parallel Scavenge/Parallel Old收集器運行示意圖

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,同樣是老年代的收集器,采用標記-清除算法。

Garbage First收集器

Garbage First(簡稱G1)收集器是垃圾收集器的一個顛覆性的產物,它開創了局部收集的設計思路和基于Region的內存布局形式。

25.什么是Stop The World ? 什么是 OopMap ?什么是安全點?

進行垃圾回收的過程中,會涉及對象的移動。為了保證對象引用更新的正確性,必須暫停所有的用戶線程,像這樣的停頓,虛擬機設計者形象描述為Stop The World。也簡稱為STW。

在HotSpot中,有個數據結構(映射表)稱為OopMap。一旦類加載動作完成的時候,HotSpot就會把對象內什么偏移量上是什么類型的數據計算出來,記錄到OopMap。在即時編譯過程中,也會在特定的位置生成 OopMap,記錄下棧上和寄存器里哪些位置是引用。

這些特定的位置主要在:

1.循環的末尾(非 counted 循環)2.方法臨返回前 / 調用方法的call指令后3.可能拋異常的位置

這些位置就叫作安全點(safepoint)。 用戶程序執行時并非在代碼指令流的任意位置都能夠在停頓下來開始垃圾收集,而是必須是執行到安全點才能夠暫停。

用通俗的比喻,假如老王去拉車,車上東西很重,老王累的汗流浹背,但是老王不能在上坡或者下坡休息,只能在平地上停下來擦擦汗,喝口水。

老王拉車只能在平路休息

26.能詳細說一下CMS收集器的垃圾收集過程嗎?

CMS收集齊的垃圾收集分為四步:

初始標記(CMS initial mark):單線程運行,需要Stop The World,標記GC Roots能直達的對象。并發標記((CMS concurrent mark):無停頓,和用戶線程同時運行,從GC Roots直達對象開始遍歷整個對象圖。重新標記(CMS remark):多線程運行,需要Stop The World,標記并發標記階段產生對象。并發清除(CMS concurrent sweep):無停頓,和用戶線程同時運行,清理掉標記階段標記的死亡的對象。

Concurrent Mark Sweep收集器運行示意圖如下:

Concurrent Mark Sweep收集器運行示意圖

27.G1垃圾收集器了解嗎?

Garbage First(簡稱G1)收集器是垃圾收集器的一個顛覆性的產物,它開創了局部收集的設計思路和基于Region的內存布局形式。

雖然G1也仍是遵循分代收集理論設計的,但其堆內存的布局與其他收集器有非常明顯的差異。以前的收集器分代是劃分新生代、老年代、持久代等。

G1把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region采用不同的策略去處理。

G1 Heap Regions

這樣就避免了收集整個堆,而是按照若干個Region集進行收集,同時維護一個優先級列表,跟蹤各個Region回收的“價值,優先收集價值高的Region。

G1收集器的運行過程大致可劃分為以下四個步驟:

初始標記(initial mark),標記了從GC Root開始直接關聯可達的對象。STW(Stop the World)執行。并發標記(concurrent marking),和用戶線程并發執行,從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象、最終標記(Remark),STW,標記再并發標記過程中產生的垃圾。篩選回收(Live Data Counting And Evacuation),制定回收計劃,選擇多個Region 構成回收集,把回收集中Region的存活對象復制到空的Region中,再清理掉整個舊 Region的全部空間。需要STW。

G1收集器運行示意圖

28.有了CMS,為什么還要引入G1?

優點:CMS最主要的優點在名字上已經體現出來——并發收集、低停頓。

缺點:CMS同樣有三個明顯的缺點。

Mark Sweep算法會導致內存碎片比較多CMS的并發能力比較依賴于CPU資源,并發回收時垃圾收集線程可能會搶占用戶線程的資源,導致用戶程序性能下降。并發清除階段,用戶線程依然在運行,會產生所謂的理“浮動垃圾”(Floating Garbage),本次垃圾收集無法處理浮動垃圾,必須到下一次垃圾收集才能處理。如果浮動垃圾太多,會觸發新的垃圾回收,導致性能降低。

G1主要解決了內存碎片過多的問題。

29.你們線上用的什么垃圾收集器?為什么要用它?

怎么說呢,雖然調優說的震天響,但是我們一般都是用默認。管你Java怎么升,我用8,那么JDK1.8默認用的是什么呢?

可以使用命令:

java -XX: PrintCommandLineFlags -version

可以看到有這么一行:

-XX: UseParallelGC

UseParallelGC = Parallel Scavenge Parallel Old,表示的是新生代用的Parallel Scavenge收集器,老年代用的是Parallel Old 收集器。

那為什么要用這個呢?默認的唄。

當然面試肯定不能這么答。

Parallel Scavenge的特點是什么?

高吞吐,我們可以回答:因為我們系統是業務相對復雜,但并發并不是非常高,所以希望盡可能的利用處理器資源,出于提高吞吐量的考慮采用Parallel Scavenge Parallel Old的組合。

當然,這個默認雖然也有說法,但不太討喜。

還可以說:

采用Parallel New CMS的組合,我們比較關注服務的響應速度,所以采用了CMS來降低停頓時間。

或者一步到位:

我們線上采用了設計比較優秀的G1垃圾收集器,因為它不僅滿足我們低停頓的要求,而且解決了CMS的浮動垃圾問題、內存碎片問題。

30.垃圾收集器應該如何選擇?

垃圾收集器的選擇需要權衡的點還是比較多的——例如運行應用的基礎設施如何?使用JDK的發行商是什么?等等……

這里簡單地列一下上面提到的一些收集器的適用場景:

Serial :如果應用程序有一個很小的內存空間(大約100 MB)亦或它在沒有停頓時間要求的單線程處理器上運行。Parallel:如果優先考慮應用程序的峰值性能,并且沒有時間要求要求,或者可以接受1秒或更長的停頓時間。CMS/G1:如果響應時間比吞吐量優先級高,或者垃圾收集暫停必須保持在大約1秒以內。ZGC:如果響應時間是高優先級的,或者堆空間比較大。31.對象一定分配在堆中嗎?有沒有了解逃逸分析技術?

對象一定分配在堆中嗎? 不一定的。

隨著JIT編譯期的發展與逃逸分析技術逐漸成熟,所有的對象都分配到堆上也漸漸變得不那么“絕對”了。其實,在編譯期間,JIT會對代碼做很多優化。其中有一部分優化的目的就是減少內存堆分配壓力,其中一種重要的技術叫做逃逸分析。

什么是逃逸分析?

逃逸分析是指分析指針動態范圍的方法,它同編譯器優化原理的指針分析和外形分析相關聯。當變量(或者對象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會被其他方法或者線程所引用,這種現象稱作指針(或者引用)的逃逸(Escape)。

通俗點講,當一個對象被new出來之后,它可能被外部所調用,如果是作為參數傳遞到外部了,就稱之為方法逃逸。

逃逸

除此之外,如果對象還有可能被外部線程訪問到,例如賦值給可以在其它線程中訪問的實例變量,這種就被稱為線程逃逸。

逃逸強度

逃逸分析的好處

棧上分配

如果確定一個對象不會逃逸到線程之外,那么久可以考慮將這個對象在棧上分配,對象占用的內存隨著棧幀出棧而銷毀,這樣一來,垃圾收集的壓力就降低很多。

同步消除

線程同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變量不會逃逸出線程,無法被其他線程訪問,那么這個變量的讀寫肯定就不會有競爭, 對這個變量實施的同步措施也就可以安全地消除掉。

標量替換

如果一個數據是基本數據類型,不可拆分,它就被稱之為標量。把一個Java對象拆散,將其用到的成員變量恢復為原始類型來訪問,這個過程就稱為標量替換。假如逃逸分析能夠證明一個對象不會被方法外部訪問,并且這個對象可以被拆散,那么可以不創建對象,直接用創建若干個成員變量代替,可以讓對象的成員變量在棧上分配和讀寫。

JVM調優32.有哪些常用的命令行性能監控和故障處理工具?操作系統工具top:顯示系統整體資源使用情況vmstat:監控內存和CPUiostat:監控IO使用netstat:監控網絡使用JDK性能監控工具jps:虛擬機進程查看jstat:虛擬機運行時信息查看jinfo:虛擬機配置查看jmap:內存映像(導出)jhat:堆轉儲快照分析jstack:Java堆棧跟蹤jcmd:實現上面除了jstat外所有命令的功能33.了解哪些可視化的性能監控和故障處理工具?

以下是一些JDK自帶的可視化性能監控和故障處理工具:

JConsole

JConsole概覽

VisualVM

VisualVM安裝插件

Java Mission Control

JMC主要界面

除此之外,還有一些第三方的工具:

MAT

Java 堆內存分析工具。

GChisto

GC 日志分析工具。

GCViewer

GC 日志分析工具。

JProfiler

商用的性能分析利器。

arthas

阿里開源診斷工具。

async-profiler

Java 應用性能分析工具,開源、火焰圖、跨平臺。

34.JVM的常見參數配置知道哪些?

一些常見的參數配置:

堆配置:

-Xms:初始堆大小-Xms:最大堆大小-XX:NewSize=n:設置年輕代大小-XX:NewRatio=n:設置年輕代和年老代的比值。如:為3表示年輕代和年老代比值為1:3,年輕代占整個年輕代年老代和的1/4-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如3表示Eden:3 Survivor:2,一個Survivor區占整個年輕代的1/5-XX:MaxPermSize=n:設置持久代大小

收集器設置:

-XX: UseSerialGC:設置串行收集器-XX: UseParallelGC:設置并行收集器-XX: UseParalledlOldGC:設置并行年老代收集器-XX: UseConcMarkSweepGC:設置并發收集器

并行收集器設置

-XX:ParallelGCThreads=n:設置并行收集器收集時使用的CPU數。并行收集線程數-XX:MaxGCPauseMillis=n:設置并行收集最大的暫停時間(如果到這個時間了,垃圾回收器依然沒有回收完,也會停止回收)-XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為:1/(1 n)-XX: CMSIncrementalMode:設置為增量模式。適用于單CPU情況-XX:ParallelGCThreads=n:設置并發收集器年輕代手機方式為并行收集時,使用的CPU數。并行收集線程數

打印GC回收的過程日志信息

-XX: PrintGC-XX: PrintGCDetails-XX: PrintGCTimeStamps-Xloggc:filename35.有做過JVM調優嗎?

JVM調優是一件很嚴肅的事情,不是拍腦門就開始調優的,需要有嚴密的分析和監控機制,大概的一個JVM調優流程圖:

JVM調優大致流程圖

實際上,JVM調優是不得已而為之,有那功夫,好好把爛代碼重構一下不比瞎調JVM強。

但是,面試官非要問怎么辦?可以從處理問題的角度來回答(對應圖中事后),這是一個中規中矩的案例:電商公司的運營后臺系統,偶發性的引發OOM異常,堆內存溢出。

1、因為是偶發性的,所以第一次簡單的認為就是堆內存不足導致,單方面的加大了堆內存從4G調整到8G -Xms8g。

2、但是問題依然沒有解決,只能從堆內存信息下手,通過開啟了-XX: HeapDumpOnOutOfMemoryError參數 獲得堆內存的dump文件。

3、用JProfiler 對 堆dump文件進行分析,通過JProfiler查看到占用內存最大的對象是String對象,本來想跟蹤著String對象找到其引用的地方,但dump文件太大,跟蹤進去的時候總是卡死,而String對象占用比較多也比較正常,最開始也沒有認定就是這里的問題,于是就從線程信息里面找突破點。

4、通過線程進行分析,先找到了幾個正在運行的業務線程,然后逐一跟進業務線程看了下代碼,有個方法引起了我的注意,導出訂單信息。

5、因為訂單信息導出這個方法可能會有幾萬的數據量,首先要從數據庫里面查詢出來訂單信息,然后把訂單信息生成excel,這個過程會產生大量的String對象。

6、為了驗證自己的猜想,于是準備登錄后臺去測試下,結果在測試的過程中發現導出訂單的按鈕前端居然沒有做點擊后按鈕置灰交互事件,后端也沒有做防止重復提交,因為導出訂單數據本來就非常慢,使用的人員可能發現點擊后很久后頁面都沒反應,然后就一直點,結果就大量的請求進入到后臺,堆內存產生了大量的訂單對象和EXCEL對象,而且方法執行非常慢,導致這一段時間內這些對象都無法被回收,所以最終導致內存溢出。

7、知道了問題就容易解決了,最終沒有調整任何JVM參數,只是做了兩個處理:

在前端的導出訂單按鈕上加上了置灰狀態,等后端響應之后按鈕才可以進行點擊后端代碼加分布式鎖,做防重處理

這樣雙管齊下,保證導出的請求不會一直打到服務端,問題解決!

36.線上服務CPU占用過高怎么排查?

問題分析:CPU高一定是某個程序長期占用了CPU資源。

CPU飆高

1、所以先需要找出那個進程占用CPU高。

top 列出系統各個進程的資源占用情況。

2、然后根據找到對應進行里哪個線程占用CPU高。

top -Hp 進程ID 列出對應進程里面的線程占用資源情況

3、找到對應線程ID后,再打印出對應線程的堆棧信息

printf "%xn" PID 把線程ID轉換為16進制。jstack PID 打印出進程的所有線程信息,從打印出來的線程信息中找到上一步轉換為16進制的線程ID對應的線程信息。

4、最后根據線程的堆棧信息定位到具體業務方法,從代碼邏輯中找到問題所在。

查看是否有線程長時間的watting 或blocked,如果線程長期處于watting狀態下, 關注watting on xxxxxx,說明線程在等待這把鎖,然后根據鎖的地址找到持有鎖的線程。

37.內存飆高問題怎么排查?

分析:內存飚高如果是發生在java進程上,一般是因為創建了大量對象所導致,持續飚高說明垃圾回收跟不上對象創建的速度,或者內存泄露導致對象無法回收。

1、先觀察垃圾回收的情況

jstat -gc PID 1000 查看GC次數,時間等信息,每隔一秒打印一次。jmap -histo PID | head -20 查看堆內存占用空間最大的前20個對象類型,可初步查看是哪個對象占用了內存。

如果每次GC次數頻繁,而且每次回收的內存空間也正常,那說明是因為對象創建速度快導致內存一直占用很高;如果每次回收的內存非常少,那么很可能是因為內存泄露導致內存一直無法被回收。

2、導出堆內存文件快照

jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆內存信息到文件。

3、使用visualVM對dump文件進行離線分析,找到占用內存高的對象,再找到創建該對象的業務代碼位置,從代碼和業務場景中定位具體問題。

38.頻繁 minor gc 怎么辦?

優化Minor GC頻繁問題:通常情況下,由于新生代空間較小,Eden區很快被填滿,就會導致頻繁Minor GC,因此可以通過增大新生代空間-Xmn來降低Minor GC的頻率。

39.頻繁Full GC怎么辦?

Full GC的排查思路大概如下:

清楚從程序角度,有哪些原因導致FGC?大對象:系統一次性加載了過多數據到內存中(比如SQL查詢未做分頁),導致大對象進入了老年代。內存泄漏:頻繁創建了大量對象,但是無法被回收(比如IO對象使用完后未調用close方法釋放資源),先引發FGC,最后導致OOM.程序頻繁生成一些長生命周期的對象,當這些對象的存活年齡超過分代年齡時便會進入老年代,最后引發FGC. (即本文中的案例)程序BUG代碼中顯式調用了gc方法,包括自己的代碼甚至框架中的代碼。JVM參數設置問題:包括總內存大小、新生代和老年代的大小、Eden區和S區的大小、元空間大小、垃圾回收算法等等。清楚排查問題時能使用哪些工具公司的監控系統:大部分公司都會有,可全方位監控JVM的各項指標。JDK的自帶工具,包括jmap、jstat等常用命令:# 查看堆內存各區域的使用率以及GC情況jstat -gcutil -h20 pid 1000# 查看堆內存中的存活對象,并按空間排序jmap -histo pid | head -n20# dump堆內存文件jmap -dump:format=b,file=heap pid可視化的堆內存分析工具:JVisualVM、MAT等排查指南查看監控,以了解出現問題的時間點以及當前FGC的頻率(可對比正常情況看頻率是否正常)了解該時間點之前有沒有程序上線、基礎組件升級等情況。了解JVM的參數設置,包括:堆空間各個區域的大小設置,新生代和老年代分別采用了哪些垃圾收集器,然后分析JVM參數設置是否合理。再對步驟1中列出的可能原因做排除法,其中元空間被打滿、內存泄漏、代碼顯式調用gc方法比較容易排查。針對大對象或者長生命周期對象導致的FGC,可通過 jmap -histo 命令并結合dump堆內存文件作進一步分析,需要先定位到可疑對象。通過可疑對象定位到具體代碼再次分析,這時候要結合GC原理和JVM參數設置,弄清楚可疑對象是否滿足了進入到老年代的條件才能下結論。40.有沒有處理過內存泄漏問題?是如何定位的?

內存泄漏是內在病源,外在病癥表現可能有:

應用程序長時間連續運行時性能嚴重下降CPU 使用率飆升,甚至到 100%頻繁 Full GC,各種報警,例如接口超時報警等應用程序拋出 OutOfMemoryError 錯誤應用程序偶爾會耗盡連接對象

嚴重內存泄漏往往伴隨頻繁的 Full GC,所以分析排查內存泄漏問題首先還得從查看 Full GC 入手。主要有以下操作步驟:

使用 jps 查看運行的 Java 進程 ID使用top -p [pid] 查看進程使用 CPU 和 MEM 的情況使用 top -Hp [pid] 查看進程下的所有線程占 CPU 和 MEM 的情況將線程 ID 轉換為 16 進制:printf "%xn" [pid],輸出的值就是線程棧信息中的nid。例如:printf "%xn" 29471,換行輸出 731f。抓取線程棧:jstack 29452 > 29452.txt,可以多抓幾次做個對比。在線程棧信息中找到對應線程號的 16 進制值,如下是 731f 線程的信息。線程棧分析可使用 Visualvm 插件 TDA。"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fbe2c164000 nid=0x731f runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE使用jstat -gcutil [pid] 5000 10 每隔 5 秒輸出 GC 信息,輸出 10 次,查看 YGC 和Full GC 次數。通常會出現 YGC 不增加或增加緩慢,而 Full GC 增加很快。或使用 jstat -gccause [pid] 5000 ,同樣是輸出 GC 摘要信息。或使用 jmap -heap [pid] 查看堆的摘要信息,關注老年代內存使用是否達到閥值,若達到閥值就會執行 Full GC。如果發現 Full GC 次數太多,就很大概率存在內存泄漏了使用 jmap -histo:live [pid] 輸出每個類的對象數量,內存大小(字節單位)及全限定類名。生成 dump 文件,借助工具分析哪 個對象非常多,基本就能定位到問題在那了使用 jmap 生成 dump 文件:# jmap -dump:live,format=b,file=29471.dump 29471Dumping heap to /root/dump ...Heap dump file created可以使用 jhat 命令分析:jhat -port 8000 29471.dump,瀏覽器訪問 jhat 服務,端口是 8000。通常使用圖形化工具分析,如 JDK 自帶的 jvisualvm,從菜單 > 文件 > 裝入 dump 文件。或使用第三方式具分析的,如 JProfiler 也是個圖形化工具,GCViewer 工具。Eclipse 或以使用 MAT 工具查看。或使用在線分析平臺 GCEasy。注意:如果 dump 文件較大的話,分析會占比較大的內存。基本上就可以定位到代碼層的邏輯了。在 dump 文析結果中查找存在大量的對象,再查對其的引用。dump 文件分析41.有沒有處理過內存溢出問題?

內存泄漏和內存溢出二者關系非常密切,內存溢出可能會有很多原因導致,內存泄漏最可能的罪魁禍首之一。

排查過程和排查內存泄漏過程類似。

虛擬機執行42.能說一下類的生命周期嗎?

一個類從被加載到虛擬機內存中開始,到從內存中卸載,整個生命周期需要經過七個階段:加載 (Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸載(Unloading),其中驗證、準備、解析三個部分統稱為連接(Linking)。

類的生命周期

43.類加載的過程知道嗎?

加載是JVM加載的起點,具體什么時候開始加載,《Java虛擬機規范》中并沒有進行強制約束,可以交給虛擬機的具體實現來自由把握。

在加載過程,JVM要做三件事情:

加載

1)通過一個類的全限定名來獲取定義此類的二進制字節流。2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。3)在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。

加載階段結束后,Java虛擬機外部的二進制字節流就按照虛擬機所設定的格式存儲在方法區之中了,方法區中的數據存儲格式完全由虛擬機實現自行定義,《Java虛擬機規范》未規定此區域的具體數據結構。

類型數據妥善安置在方法區之后,會在Java堆內存中實例化一個java.lang.Class類的對象, 這個對象將作為程序訪問方法區中的類型數據的外部接口。

44.類加載器有哪些?

主要有四種類加載器:

啟動類加載器(Bootstrap ClassLoader)用來加載java核心類庫,無法被java程序直接引用。擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過ClassLoader.getSystemClassLoader()來獲取它。用戶自定義類加載器 (user class loader),用戶通過繼承 java.lang.ClassLoader類的方式自行實現的類加載器。45.什么是雙親委派機制?

雙親委派模型

雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去完成加載。

46.為什么要用雙親委派機制?

答案是為了保證應用程序的穩定有序。

例如類java.lang.Object,它存放在rt.jar之中,通過雙親委派機制,保證最終都是委派給處于模型最頂端的啟動類加載器進行加載,保證Object的一致。反之,都由各個類加載器自行去加載的話,如果用戶自己也編寫了一個名為java.lang.Object的類,并放在程序的 ClassPath中,那系統中就會出現多個不同的Object類。

47.如何破壞雙親委派機制?

如果不想打破雙親委派模型,就重寫ClassLoader類中的fifindClass()方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。而如果想打破雙親委派模型則需要重寫loadClass()方法。

48.歷史上有哪幾次雙親委派機制的破壞?

雙親委派機制在歷史上主要有三次破壞:

雙親委派模型的三次破壞

第一次破壞

雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK 1.2面世以前的“遠古”時代。

由于雙親委派模型在JDK 1.2之后才被引入,但是類加載器的概念和抽象類 java.lang.ClassLoader則在Java的第一個版本中就已經存在,為了向下兼容舊代碼,所以無法以技術手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一個新的 protected方法findClass(),并引導用戶編寫的類加載邏輯時盡可能去重寫這個方法,而不是在 loadClass()中編寫代碼。

第二次破壞

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的,如果有基礎類型又要調用回用戶的代碼,那該怎么辦呢?

例如我們比較熟悉的JDBC:

各個廠商各有不同的JDBC的實現,Java在核心包lib里定義了對應的SPI,那么這個就毫無疑問由啟動類加載器加載器加載。

但是各個廠商的實現,是沒辦法放在核心包里的,只能放在classpath里,只能被應用類加載器加載。那么,問題來了,啟動類加載器它就加載不到廠商提供的SPI服務代碼。

為了解決這個問題,引入了一個不太優雅的設計:線程上下文類加載器 (Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContext-ClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。

JNDI服務使用這個線程上下文類加載器去加載所需的SPI服務代碼,這是一種父類加載器去請求子類加載器完成類加載的行為。

第三次破壞

雙親委派模型的第三次“被破壞”是由于用戶對程序動態性的追求而導致的,例如代碼熱替換(Hot Swap)、模塊熱部署(Hot Deployment)等。

OSGi實現模塊化熱部署的關鍵是它自定義的類加載器機制的實現,每一個程序模塊(OSGi中稱為 Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。在OSGi環境下,類加載器不再雙親委派模型推薦的樹狀結構,而是進一步發展為更加復雜的網狀結構。

49.你覺得應該怎么實現一個熱部署功能?

我們已經知道了Java類的加載過程。一個Java類文件到虛擬機里的對象,要經過如下過程:首先通過Java編譯器,將Java文件編譯成class字節碼,類加載器讀取class字節碼,再將類轉化為實例,對實例newInstance就可以生成對象。

類加載器ClassLoader功能,也就是將class字節碼轉換到類的實例。在Java應用中,所有的實例都是由類加載器,加載而來。

一般在系統中,類的加載都是由系統自帶的類加載器完成,而且對于同一個全限定名的java類(如com.csiar.soc.HelloWorld),只能被加載一次,而且無法被卸載。

這個時候問題就來了,如果我們希望將java類卸載,并且替換更新版本的java類,該怎么做呢?

既然在類加載器中,Java類只能被加載一次,并且無法卸載。那么我們是不是可以直接把Java類加載器干掉呢?答案是可以的,我們可以自定義類加載器,并重寫ClassLoader的findClass方法。

想要實現熱部署可以分以下三個步驟:

銷毀原來的自定義ClassLoader更新class類文件創建新的ClassLoader去加載更新后的class類文件。

到此,一個熱部署的功能就這樣實現了。

50.Tomcat的類加載機制了解嗎?

Tomcat是主流的Java Web服務器之一,為了實現一些特殊的功能需求,自定義了一些類加載器。

Tomcat類加載器如下:

Tomcat類加載器

Tomcat實際上也是破壞了雙親委派模型的。

Tomact是web容器,可能需要部署多個應用程序。不同的應用程序可能會依賴同一個第三方類庫的不同版本,但是不同版本的類庫中某一個類的全路徑名可能是一樣的。如多個應用都要依賴hollis.jar,但是A應用需要依賴1.0.0版本,但是B應用需要依賴1.0.1版本。這兩個版本中都有一個類是com.hollis.Test.class。如果采用默認的雙親委派類加載機制,那么無法加載多個相同的類。

所以,Tomcat破壞了雙親委派原則,提供隔離的機制,為每個web容器單獨提供一個WebAppClassLoader加載器。每一個WebAppClassLoader負責加載本身的目錄下的class文件,加載不到時再交CommonClassLoader加載,這和雙親委派剛好相反。

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如有侵權行為,請第一時間聯系我們修改或刪除,多謝。

CopyRight ? 外貿領航 2023 All Rights Reserved.