在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方法??梢哉J為方法區就是永久代。


堆(Heap)


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


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


新生代又被劃分為三個區域:Eden和兩個幸存區。


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


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


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




引用

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




java內存管理分為內存分配和內存回收,不需要程序員參與。

垃圾回收機制主要看對象是否有引用指向。java對象的引用包括強引用、軟引用、弱引用和虛引用。

強引用:是指創建一個對象,并把這個對象賦給一個引用變量。強引用有引用變量指向時永遠都不會被回收,即使內存不足時。

軟引用:通過SoftReference類來實現,當系統內存充足時,系統不會進行軟引用的內存回收,軟引用的對象和強引用沒有太多區別,但是內存不足時會回收軟引用的對象。

弱引用:通過WeakReference類來實現,具有很強的不確定性,因為垃圾回收每次都會回收弱引用的對象。

虛引用:軟引用和弱引用都可以多帶帶使用,虛引用不能多帶帶使用,必須關聯引用隊列。虛引用的作用就是跟蹤對象被垃圾回收的狀態,程序可以通過檢測與虛引用關聯的虛引用隊列是否已經包含了指定的虛引用,從而了解虛引用對象是否即將被回收。它允許你知道對象何時從內存中移除。

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


垃圾回收

1、引用計數器算法:當創建對象時,為這個對象在堆??臻g中分配地址,同時會產生一個引用計數器,同時引用計數器+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.