關於JVM面試所必須知道的內容

引自:https://www.cnblogs.com/wbyp/p/7753528.html

  • 在Java中主要有以下三種類加載器

    引導類加載器(bootstrap class loader)

        --用來加載java的核心庫(String,Integer,List......)在jre/lib/rt.jar路徑下的內容。使用c代碼來實現的,並不繼承自java.lang.ClassLoader.

        --加載擴展類加載器和應用程序加載器,並指定他們的父類加載器。

    擴展類加載器(extensions class loader)

        --用來加載java的擴展庫(jre/ext/*.jar路徑下的內容),java虛擬機的實現會自動提供一個擴展目錄。該類加載器在此目錄裏面查找並加載java類。

    應用程序類加載器(application class loader)

        --它根據java應用的類路徑(classpath路徑),一般來說java應用的類都是由它來完成加載的。

    自定義類加載器

        --開發人員可以通過繼承java.lang.ClassLoader類的方式實現在即的類加載器,以滿足一些特殊的要求。

擴展類加載器、應用程序類加載器和自定義類加載器都是由java實現,都繼承java.lang.ClassLoader類。

  • 類加載器的代理模式:雙親委託機制

    當某個類加載器在接收到加載類的請求後,首先將加載任務委託給父類加載器,依次追溯,如果父類加載器能夠完成類加載任務,就成功返回,只有父類加載器無法完成加載任務是,才自己加載。

    雙親機制是爲了保證java核心庫的類型安全,不會出現用戶能自定義java.lang.Object類的情況。

    雙親委託機制是代理模式的一種,並不是所有類加載器都採用雙親委託機制,Tomcat服務器類加載器也使用代理模式,不同的是它是首先嚐試自己去加載某個類,如果找不到再代理給父類加載器。

  • 類加載機制

    jvm把class文件加載到內存,並對數據進行校驗、解析和初始化,最終形成jvm可以直接使用的java類型的過程。

    類加載過程:類從被加載到虛擬機內存中開始,直到卸載出內存爲止,它的整個生命週期包括7個階段:加載、驗證、準備、解析、初始化、使用、卸載(其中驗證、準備和解析這三個部分統稱爲連接)。其中加載、驗證、準備、初始化和卸載這五個階段的順序是一定的,而解析階段不一定,在某種情況下,可以在初始化之後再開始,這是爲了支持java語言的運行時綁定。

  1. 加載:將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法區中的運行時數據結構,在堆中生成一個代表這個類的java.lang.Class對象,作爲方法區類數據的訪問入口。
  2. 連接:將java類的二進制代碼合併到jvm的運行狀態之中的過程。驗證:確保加載的類信息符合jvm規範,沒有安全方面的問題。準備:正式爲類變量(static變量)分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行。解析:虛擬機常量池內的符號引用替換爲直接引用的過程。(比如String s = "aaa",轉化爲s的地址指向"aaa"的地址)。
  3. 初始化:初始化階段是執行類構造器方法的過程,類構造器方法是由編譯器自動收集類中的所有變量的賦值動作和靜態語句塊(static塊)中的語句合併產生的。當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先進行其父類的初始化,虛擬機會保證一個類的構造器方法在多線程環境中被正確加鎖和同步。當訪問一個java類的靜態域時,只有真正申明這個靜態變量的類纔會被初始化。
  • 類的加載過程分爲:類的主動引用和類的被動引用

    類的主動引用(一定會發生類的初始化):

        --new一個類的對象

        --調用類的靜態成員(除了final常量)和靜態方法

        --使用java.lang.reflect包的方法對類進行反射調用

        --當初始化一個類,如果父類沒有被初始化,先初始化其父類

        --當要執行某個程序時,一定先啓動main方法所在的類

    類的被動引用(不會發生類的初始化)

        --當訪問一個靜態變量時,只有真正聲明這個靜態變量的類纔會初始化(通過子類引用父類的靜態變量,不會造成子類的初始化)

        --通過數組定義類應用,不會觸發此類的初始化A[] a = new A[10];

        --引用常量(final類型)不會觸發此類的初始化(常量在編譯階段就存入調用類的常量池中了)

  • java中類的加載順序
  1. 虛擬機在首次加載java類時,會對靜態初始化塊、靜態成員變量、靜態方法進行一次初始化
  2. 只有在調用new方法時,纔會創建類的實例
  3. 類實例創建過程:首先執行父類的初始化塊部分,然後是父類的構造方法,再執行子類的初始化塊,最後是子類的構造方法
  4. 類實例銷燬時,先銷燬子類部分,再銷燬父類部分。
  • java程序執行過程

    首先java源代碼文件(.java)會被java編譯爲字節碼文件(.class),然後由jvm中的類加載器加載各個類的字節碼文件,加載完畢之後,交由jvm執行引擎執行。

  • jvm區域劃分

    jvm區域可以根據線程分成線程隔離和線程共享兩個部分,其中線程隔離即這些區域是線程獨有的,每個線程都會分配這樣的區域,包括程序計數器、Java棧和本地方法棧;線程共享的有方法區和堆。

    程序計數器(Program Counter Register)

    由於在JVM中,多線程是通過線程輪流切換來獲得CPU執行時間的,因此在任意具體時刻,一個CPU只會執行一個線程中的指令,爲了能夠使得每個線程都在線程切換或能夠恢復到切換之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,並且不能互相被幹擾,否認就會影響到程序的正常執行次序。所以程序計數器是每個線程所私有的。在jvm規範中規定,如果線程執行的是非native方法,則程序計數器中保存的是當前需要執行的指令的地址;如果線程執行的是native方法,則程序計數器中保存的值是undefined。

    java棧(vm stack)

    java棧也稱爲虛擬機棧(java vitual machine stack),java棧中存放的是一個個棧幀,每個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(local variables)、操作數棧(perand stack)、指向當前方法所屬的類的運行時常量池的引用、方法返回地址和一些額外的附加信息。

    本地方法棧(native method stack)

    本地方法棧與java棧的作用和原理非常相似,只不過java棧是爲執行java方法服務,而本地方法棧是爲執行本地方法服務的。在jvm規範中,並沒有對本地方法的具體實現方法以及數據結構做強制規定,虛擬機可以自由實現它。在Hotsopt虛擬機中直接就把本地方法棧和java棧合二爲一。

    方法區(Method Area)

    方法區在JVM中是一個非常重要的區域,與堆一樣是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法、字段信息)、靜態變量、常量以及編譯器編譯後的代碼。在方法區有一個非常重要的部分就是運行時常量池它是每一個類或者接口的常量池的運行時表示形式,在類和接口被加載到jvm後,對應的運行時常量池就被創建出來。當然並非Class文件常量池中的內容才能進入運行時常量池,在運行期間,也可將新的常量放入運行時常量池中,比如String的intern方法。可以認爲方法區就是永久代。

    堆(Heap)

    java中的堆是用來存儲對象以及數組,數組的引用是存放在java棧中的。堆被所有線程共享,在jvm中只有一個堆。

    在java中,堆被劃分成兩個不同的區域:新生代(Young)、老年代(Old)。

    新生代又被劃分爲三個區域:Eden和兩個倖存區

    這樣劃分的目的是爲了使JVM能夠更好地管理堆內存中的對象,包括內存的分配及回收。

    新生代主要存儲新創建的對象和尚未進入老年代的對象。老年代存儲經過多次新生代GC(Minor GC)後仍然存活的對象。

    方法區主要存放類與類之間關係的數據,這部分數據被加載到內存以後,基本上不會發生變更,但是後期方法區也會被回收,回收的條件非常的苛刻;java堆中的數據基本上是朝生夕死的,用完之後就會被回收;java棧和本地方法棧中的數據,滿足先進後出的原則,當要獲取棧低的元素,必須把棧頂的元素出棧,回收率爲100%;程序計數器是唯一一塊不會內存溢出的區域。

  • 引用

    java中如果一個對象,沒有一個引用指向它,那麼它就被認爲是一個垃圾。

  1. java內存管理分爲內存分配和內存回收,不需要程序員參與。
  2. 垃圾回收機制主要看對象是否有引用指向。java對象的引用包括強引用軟引用弱引用虛引用
  3. 強引用:是指創建一個對象,並把這個對象賦給一個引用變量。強引用有引用變量指向時永遠都不會被回收,即使內存不足時。
  4. 軟引用:通過SoftReference類來實現,當系統內存充足時,系統不會進行軟引用的內存回收,軟引用的對象和強引用沒有太多區別,但是內存不足時會回收軟引用的對象。
  5. 弱引用:通過WeakReference類來實現,具有很強的不確定性,因爲垃圾回收每次都會回收弱引用的對象。
  6. 虛引用:軟引用和弱引用都可以單獨使用,虛引用不能單獨使用,必須關聯引用隊列。虛引用的作用就是跟蹤對象被垃圾回收的狀態,程序可以通過檢測與虛引用關聯的虛引用隊列是否已經包含了指定的虛引用,從而瞭解虛引用對象是否即將被回收。它允許你知道對象何時從內存中移除。

    java中引用越弱表示對垃圾回收器的限制越少,對象越容易被回收。

  • 垃圾回收

    1、引用計數器算法:當創建對象時,爲這個對象在堆棧空間中分配地址,同時會產生一個引用計數器,同時引用計數器+1,當有新的引用的時候,引用計數器繼續+1,而當其中一個引用銷燬時,引用計數器-1,當引用計數器被減爲0的時候,標誌着這個對象已經沒有引用了,可以被回收。但是當代碼出現下面的情形時,該算法無法適用,objA指向objB,而objB又指向objA,這樣其他所有引用都消失了之後,objA和ObjB還是有一個相互的引用,無法回收,但實際上這兩個對象都已經沒有額外的引用了,已經是垃圾了。

ObjA.obj = ObjB;
ObjB.obj = ObjA;

    2、根搜索算法(GC Root):把所有的引用關係看做一張圖,從一個節點GC Root開始,尋找對應的引用節點,找到這個節點以後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點。java中可作爲GC Root的對象有:虛擬機棧中的引用對象、方法區中靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中引用的對象。

    3、收集後的垃圾通過什麼算法來回收?

  • 標記-清除算法:採用從根集合進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收。標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活對象比較多的情況下極爲高效,但是由於標記-清除算法直接回收不存活的對象,因此會造成內存碎片。
  • 複製算法(用於新生代):複製算法採用從根集合掃描,並將存活對象複製到一塊新的、沒有使用過的空間中,這種算法當內存中存活的對象比較少時,極爲高效,但是帶來的成本是需要一塊內存交換空間用於進行對象的移動。複製算法中,新生代中每次只使用Eden區和一塊倖存區存儲數據,當倖存區達到飽和狀態時,將倖存區的存活的對象移動到另一塊倖存區。
  • 標記-整理算法(用於老年代):標記-整理算法和標記-清除算法採用一樣的方式進行對象的標記,但是清除時不同,在回收不存活的對象佔用的空間後,會將所有存活的對象王左端空閒空間移動,並更新對應的指針。解決了內存碎片的問題。
  • 分代回收機制

    新生代:絕大多數最新被創建的對象會被分配到這裏,由於大部分對象在創建後會很快變得不可達,所以很多對象被創建在新生代,然後消失。對象此區域消失的過程稱爲“minor GC”.

    一共有三個空間,其中包含一個伊甸園區(Eden)和兩個倖存區(survivor)。各空間執行順序如下:

    1、絕大多數剛剛被創建的對象會存放在伊甸園空間。

    2、在伊甸園空間執行了一次 GC後,存活的對象被移動到其中一個倖存者空間。

    3、此後,在伊甸園空間執行GC後,存活的對象會被堆積在同一個倖存者空間。

    4、當一個倖存者空間飽和戶,還在存活的對象會被移動到另一個倖存者空間,之後會清空已經飽和的那個倖存者空間。

    5、在以上的步驟中重複幾次依然存活的對象就會被移動到老年代。

    老年代:對象沒有變的不可達,並且從新生代中存活下來,就會被拷貝到這裏,其所佔的空間要比新生代多。也正是因爲其相對較大的空間,發生在老年代上的GC要比新生代少得多。對象從老年代中消失的過程,稱爲“major GC”。

    永久代:也被稱爲方法區,用來保存類常量以及字符串常量。因此這個區域不是用來永久的存儲那些從老年代存活下來的對象。這個區域也可能發生GC,並且發生在這個區域上的GC時間也被稱爲major GC.


發佈了49 篇原創文章 · 獲贊 19 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章