關於JVM面試所必須知道的內容
- 在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語言的執行時繫結。
- 載入:將class檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區中的執行時資料結構,在堆中生成一個代表這個類的java.lang.Class物件,作為方法區類資料的訪問入口。
- 連線:將java類的二進位制程式碼合併到jvm的執行狀態之中的過程。驗證:確保載入的類資訊符合jvm規範,沒有安全方面的問題。準備:正式為類變數(static變數)分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行。解析:虛擬機器常量池內的符號引用替換為直接引用的過程。(比如String s = "aaa",轉化為s的地址指向"aaa"的地址)。
- 初始化:初始化階段是執行類構造器方法的過程,類構造器方法是由編譯器自動收集類中的所有變數的賦值動作和靜態語句塊(static塊)中的語句合併產生的。當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先進行其父類的初始化,虛擬機器會保證一個類的構造器方法在多執行緒環境中被正確加鎖和同步。當訪問一個java類的靜態域時,只有真正申明這個靜態變數的類才會被初始化。
- 類的載入過程分為:類的主動引用和類的被動引用
類的主動引用(一定會發生類的初始化):
--new一個類的物件
--呼叫類的靜態成員(除了final常量)和靜態方法
--使用java.lang.reflect包的方法對類進行反射呼叫
--當初始化一個類,如果父類沒有被初始化,先初始化其父類
--當要執行某個程式時,一定先啟動main方法所在的類
類的被動引用(不會發生類的初始化)
--當訪問一個靜態變數時,只有真正宣告這個靜態變數的類才會初始化(通過子類引用父類的靜態變數,不會造成子類的初始化)
--通過陣列定義類應用,不會觸發此類的初始化A[] a = new A[10];
--引用常量(final型別)不會觸發此類的初始化(常量在編譯階段就存入呼叫類的常量池中了)
- java中類的載入順序
- 虛擬機器在首次載入java類時,會對靜態初始化塊、靜態成員變數、靜態方法進行一次初始化
- 只有在呼叫new方法時,才會建立類的例項
- 類例項建立過程:首先執行父類的初始化塊部分,然後是父類的構造方法,再執行子類的初始化塊,最後是子類的構造方法
- 類例項銷燬時,先銷燬子類部分,再銷燬父類部分。
- 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中如果一個物件,沒有一個引用指向它,那麼它就被認為是一個垃圾。
- java記憶體管理分為記憶體分配和記憶體回收,不需要程式設計師參與。
- 垃圾回收機制主要看物件是否有引用指向。java物件的引用包括強引用、軟引用、弱引用和虛引用。
- 強引用:是指建立一個物件,並把這個物件賦給一個引用變數。強引用有引用變數指向時永遠都不會被回收,即使記憶體不足時。
- 軟引用:通過SoftReference類來實現,當系統記憶體充足時,系統不會進行軟引用的記憶體回收,軟引用的物件和強引用沒有太多區別,但是記憶體不足時會回收軟引用的物件。
- 弱引用:通過WeakReference類來實現,具有很強的不確定性,因為垃圾回收每次都會回收弱引用的物件。
- 虛引用:軟引用和弱引用都可以單獨使用,虛引用不能單獨使用,必須關聯引用佇列。虛引用的作用就是跟蹤物件被垃圾回收的狀態,程式可以通過檢測與虛引用關聯的虛引用佇列是否已經包含了指定的虛引用,從而瞭解虛引用物件是否即將被回收。它允許你知道物件何時從記憶體中移除。
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.