Java 底層機(jī)制

JVM體系結(jié)構(gòu)

JVM是一種解釋執(zhí)行class文件的規(guī)范技術(shù)。

JVM體系結(jié)構(gòu)

我翻譯的中文圖:

中文圖

類裝載器子系統(tǒng)

在JVM中負(fù)責(zé)裝載.class文件(一種8位二進(jìn)制流文件,各個數(shù)據(jù)項(xiàng)按順序緊密的從前向后排列届氢, 相鄰的項(xiàng)之間沒有間隙顷霹,經(jīng)編譯器編譯.java源文件后生成翘盖,每個類(或者接口)都單獨(dú)占有一個class文件)朦蕴。

運(yùn)行時數(shù)據(jù)區(qū)

方法區(qū)

當(dāng)JVM使用類裝載器定位class文件,并將其輸入到內(nèi)存中時刊愚。會提取class文件的類型信息踊跟,并將這些信息存儲到方法區(qū)中。同時放入方法區(qū)中的還有該類型中的類靜態(tài)變量百拓。

  • 該類型的全限定名琴锭。如java.io.FileOutputStream
  • 該類型的直接超類的全限定名。如java.io.OutputStream
  • 該類型是類類型還是接口類型衙传。
  • 該類型的訪問修飾符(public决帖、abstractfinal)蓖捶。
  • 任何直接超接口的全限定名的有序列表地回。如java.io.Closeable, java.io.Flushable
  • 該類型的常量池俊鱼。比如所有類型(Class)刻像、方法、字段的符號并闲、基本數(shù)據(jù)類型的直接數(shù)值(final)等细睡。
  • 字段信息:對類型中聲明的每個字段。
  • 方法信息帝火。
  • 類靜態(tài)變量:靜態(tài)變量而不是放在堆里面溜徙,所以靜態(tài)屬于類,不屬于對象犀填。
  • 指向ClassLoader類的引用蠢壹。
  • 指向Class類的引用。
  • 方法表:為了能快速定位到類型中的某個方法九巡,JVM對每個裝載的類型都會建立一個方法表图贸,用于存儲該類型對象可以調(diào)用的方法的直接引用,這些方法就包括從超類中繼承來的冕广。而這張表與Java動態(tài)綁定機(jī)制的實(shí)現(xiàn)是密切相關(guān)的疏日。

常量池

常量池指的是在編譯期被確定,并被保存在已編譯的.class文件中的一些數(shù)據(jù)撒汉。除了包含代碼中所定義的各種基本數(shù)據(jù)類型和對象型(String及數(shù)組)的常量值(final制恍,在編譯時確定,并且編譯器會優(yōu)化)還包含一些以文本形式出現(xiàn)的符號引用(類信息)神凑,比如:

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法和名稱和描述符

虛擬機(jī)必須給每個被裝載的類型維護(hù)一個常量池。常量池就是該類型所用到常量的一個有序集合,包括直接常量(string溉委、integer等)和其他類型鹃唯,字段和方法的符號引用

方法區(qū)是多線程共享的瓣喊。也就是當(dāng)虛擬機(jī)實(shí)例開始運(yùn)行程序時坡慌,邊運(yùn)行邊加載進(jìn)class文件。不同的Class文件都會提取出不同類型信息存放在方法區(qū)中藻三。同樣洪橘,方法區(qū)中不再需要運(yùn)行的類型信息會被垃圾回收線程丟棄掉

堆內(nèi)存

Java 程序在運(yùn)行時創(chuàng)建的所有類型對象和數(shù)組都存儲在堆中棵帽。JVM會根據(jù)new指令在堆中開辟一個確定類型的對象內(nèi)存空間熄求。但是堆中開辟對象的空間并沒有任何人工指令可以回收,而是通過JVM的垃圾回收器負(fù)責(zé)回收逗概。

  • 堆中對象存儲的是該對象以及對象所有超類的實(shí)例數(shù)據(jù)(但不是靜態(tài)數(shù)據(jù))弟晚。
  • 其中一個對象的引用可能在整個運(yùn)行時數(shù)據(jù)區(qū)中的很多地方存在,比如Java棧逾苫,堆卿城,方法區(qū)等
  • 堆中對象還應(yīng)該關(guān)聯(lián)一個對象的鎖數(shù)據(jù)信息以及線程的等待集合(線程等待池)铅搓。這些都是實(shí)現(xiàn)Java線程同步機(jī)制的基礎(chǔ)瑟押。
  • java中數(shù)組也是對象,那么自然在堆中會存儲數(shù)組的信息星掰。

程序計(jì)數(shù)器

對于一個運(yùn)行的Java而言多望,每一個線程都有一個PC寄存器。當(dāng)線程執(zhí)行Java程序時蹋偏,PC寄存器的內(nèi)容總是下一條將被執(zhí)行的指令地址便斥。

Java棧

每啟動一個線程JVM都會為它分配一個Java棧威始,用于存放方法中的局部變量枢纠,操作數(shù)以及異常數(shù)據(jù)等。當(dāng)線程調(diào)用某個方法時黎棠,JVM會根據(jù)方法區(qū)中該方法的字節(jié)碼組建一個棧幀晋渺。并將該棧幀壓入Java棧中,方法執(zhí)行完畢時脓斩,JVM會彈出該棧幀并釋放掉木西。

注意Java棧中的數(shù)據(jù)是線程私有的,一個線程是無法訪問另一個線程的Java棧的數(shù)據(jù)随静。這也就是為什么多線程編程時八千,兩個相同線程執(zhí)行同一方法時吗讶,對方法內(nèi)的局部變量是不需要數(shù)據(jù)同步的原因

java棧和局部變量詳解

成員變量有默認(rèn)值(被final修飾且沒有static的必須顯式賦值)恋捆,局部變量不會自動賦值照皆。

執(zhí)行引擎

運(yùn)行Java的每一個線程都是一個獨(dú)立的虛擬機(jī)執(zhí)行引擎的實(shí)例。從線程生命周期的開始到結(jié)束沸停,他要么在執(zhí)行字節(jié)碼膜毁,要么在執(zhí)行本地方法。一個線程可能通過解釋或者使用芯片級指令直接執(zhí)行字節(jié)碼愤钾,或者間接通過JIT(即時編譯器)執(zhí)行編譯過的本地代碼瘟滨。

注意JVM是進(jìn)程級別,執(zhí)行引擎是線程級別能颁。

指令集

實(shí)際上杂瘸,class文件中方法的字節(jié)碼流就是有JVM的指令序列構(gòu)成的。每一條指令包含一個單字節(jié)的操作碼劲装,后面跟隨0個或多個操作數(shù)胧沫。

指令由一個操作碼和零個或多個操作數(shù)組成。

iload_0    // 把存儲在局部變量區(qū)中索引為0的整數(shù)壓入操作數(shù)棧占业。
iload_1    // 把存儲在局部變量區(qū)中索引為1的整數(shù)壓入操作數(shù)棧绒怨。
iadd         // 從操作數(shù)棧中彈出兩個整數(shù)相加,在將結(jié)果壓入操作數(shù)棧谦疾。  
istore_2   // 從操作數(shù)棧中彈出結(jié)果  

很顯然南蹂,上面的指令反復(fù)用到了Java棧中的某一個方法棧幀。實(shí)際上執(zhí)行引擎運(yùn)行Java字節(jié)碼指令很多時候都是在不停的操作Java棧念恍,也有的時候需要在堆中開辟對象以及運(yùn)行系統(tǒng)的本地指令等六剥。但是Java棧的操作要比堆中的操作要快的多,因此反復(fù)開辟對象是非常耗時的峰伙。這也是為什么Java程序優(yōu)化的時候疗疟,盡量減少new對象。

示例分析

//源代碼 Test.java  
package edu.hr.jvm;  
  
import edu.hr.jvm.bean;  
public class Test{  
       public static void main(String[] args){  
               Act act=new Act();  
               act.doMathForever();  
       }  
}  
  
//源代碼 Act.java  
package edu.hr.jvm.bean;  
  
public class Act{  
       public void doMathForever(){  
              int i=0;  
              for(;;){  
                     i+=1;  
                     i*=2;   
              }  
       }  
}  
示例
  • 首先OS會創(chuàng)建一個JVM實(shí)例(進(jìn)行必要的初始化工作瞳氓,比如:初始啟動類裝載器策彤,初始運(yùn)行時內(nèi)存數(shù)據(jù)區(qū)等。

  • 然后通過自定義類裝載器加載Test.class匣摘。并提取Test.class字節(jié)碼中的信息存放在方法區(qū) 中(具體的信息在上面已經(jīng)講過)店诗。上圖展示了方法區(qū)中的Test類信息,其中在常量池中有一個符號引用“Act”(類的全限定名音榜,注意:這個引用目前還沒有真正的類信息的內(nèi)存地址)庞瘸。

  • 接著JVM開始從Test類的main字節(jié)碼處開始解釋執(zhí)行。在運(yùn)行之前赠叼,會在Java棧中組建一個main方法的棧幀 擦囊,如上圖Java棧所示违霞。JVM需要運(yùn)行任何方法前,通過在Java棧中壓入一個幀棧瞬场。在這個幀棧的內(nèi)存區(qū)域中進(jìn)行計(jì)算葛家。

  • 現(xiàn)在可以開始執(zhí)行main方法的第一條指令 —— JVM需要為常量池的第一項(xiàng)的類(符號引用Act)分配內(nèi)存空間。但是Act類此時還沒有加載進(jìn)JVM(因?yàn)槌A砍啬壳爸挥幸粋€“Act”的符號引用)泌类。

  • JVM加載進(jìn)Act.class,并提取Act類信息放入方法區(qū)中底燎。然后以一個直接指向方法區(qū)Act類信息的直接引用(在棧中)換開始在常量池中的符號引用“Act”刃榨,這個過程就是常量池解析 。以后就可以直接訪問Act的類信息了双仍。

  • 此時JVM可以根據(jù)方法區(qū)中的Act類信息枢希,在堆中開辟一個Act類對象act

  • 接著開始執(zhí)行main方法中的第二條指令調(diào)用doMathForever方法朱沃。這個可以通過堆中act對象所指的方法表中查找苞轿,然后定位到方法區(qū)中的Act類信息中的doMathForever方法字節(jié)碼。在運(yùn)行之前逗物,仍然要組建一個doMathForever棧幀壓入Java棧搬卒。(注意:JVM會根據(jù)方法區(qū)中doMathForever的字節(jié)碼來創(chuàng)建棧幀的局部變量區(qū)操作數(shù)棧的大小)

  • 接下來JVM開始解釋運(yùn)行Act.doMathForever字節(jié)碼的內(nèi)容了翎卓。

編譯和運(yùn)行過程

編譯:源碼要運(yùn)行契邀,必須先轉(zhuǎn)成二進(jìn)制的機(jī)器碼。這是編譯器的任務(wù)失暴。

  • 源文件由編譯器編譯成字節(jié)碼坯门。 創(chuàng)建完源文件之后,程序會先被編譯為.class文件逗扒。Java編譯一個類時古戴,如果這個類所依賴的類還沒有被編譯,編譯器就會先編譯這個被依賴的類矩肩,然后引用现恼,否則直接引用。如果java編譯器在指定目錄下找不到該類所其依賴的類的.class文件或者.java源文件的話蛮拔,編譯器話報(bào)“cant find symbol”的錯誤述暂。
  • 編譯后的字節(jié)碼文件格式主要分為兩部分:常量池和方法字節(jié)碼。常量池記錄的是代碼出現(xiàn)過的所有token(類名建炫,成員變量名等等)以及符號引用(方法引用畦韭,成員變量引用等等);方法字節(jié)碼放的是類中各個方法的字節(jié)碼肛跌。

運(yùn)行java類運(yùn)行的過程大概可分為兩個過程:類的加載艺配,類的執(zhí)行察郁。需要說明的是:JVM主要在程序第一次主動使用類的時候,才會去加載該類转唉。也就是說皮钠,JVM并不是在一開始就把一個程序就所有的類都加載到內(nèi)存中,而是到不得不用的時候才把它加載進(jìn)來赠法,而且只加載一次麦轰。

下面是程序運(yùn)行的詳細(xì)步驟

//MainApp.java  
public class MainApp {  
    public static void main(String[] args) {  
        Animal animal = new Animal("Puppy");  
        animal.printName();  
    }  
}  
//Animal.java  
public class Animal {  
    public String name;  
    public Animal(String name) {  
        this.name = name;  
    }  
    public void printName() {  
        System.out.println("Animal ["+name+"]");  
    }  
}  
  • 在編譯好java程序得到MainApp.class文件后,在命令行上敲java AppMain砖织。系統(tǒng)就會啟動一個jvm進(jìn)程款侵,jvm進(jìn)程classpath路徑中找到一個名為AppMain.class的二進(jìn)制文件,將MainApp的類信息加載到運(yùn)行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi)侧纯,這個過程叫做MainApp類的加載新锈。
  • 然后JVM找到AppMain的主函數(shù)入口,開始執(zhí)行main函數(shù)眶熬。
  • main函數(shù)的第一條命令是Animal animal = new Animal("Puppy");就是讓JVM創(chuàng)建一個Animal對象妹笆,但是這時候方法區(qū)中沒有Animal類的信息,所以JVM馬上加載Animal類娜氏,把Animal類的類型信息放到方法區(qū)中拳缠。
  • 加載完Animal類之后,Java虛擬機(jī)做的第一件事情就是在堆區(qū)中為一個新的Animal實(shí)例分配內(nèi)存牍白,然后調(diào)用構(gòu)造函數(shù)初始化Animal實(shí)例脊凰,這個Animal實(shí)例持有著指向方法區(qū)的Animal類的類型信息(其中包含有方法表,java動態(tài)綁定的底層實(shí)現(xiàn))的引用茂腥。
  • 當(dāng)使用animal.printName()的時候狸涌,JVM根據(jù)animal引用找到Animal對象,然后根據(jù)Animal對象持有的引用定位到方法區(qū)中Animal類的類型信息的方法表最岗,獲得printName()函數(shù)的字節(jié)碼的地址帕胆。
  • 開始運(yùn)行printName()函數(shù)的字節(jié)碼(可以把字節(jié)碼理解為一條條的指令)。
    圖示

特別說明:java類中所有public和protected的實(shí)例方法都采用動態(tài)綁定機(jī)制般渡,所有私有方法懒豹、靜態(tài)方法構(gòu)造器初始化方法<clinit>都是采用靜態(tài)綁定機(jī)制驯用。而使用動態(tài)綁定機(jī)制的時候會用到方法表脸秽,靜態(tài)綁定時并不會用到。

通過前面的兩個例子的分析蝴乔,應(yīng)該理解了不少了吧记餐。

類加載機(jī)制

JVM主要包含三大核心部分:類加載器,運(yùn)行時數(shù)據(jù)區(qū)和執(zhí)行引擎薇正。

虛擬機(jī)將描述類的數(shù)據(jù)從class文件加載到內(nèi)存片酝,并對數(shù)據(jù)進(jìn)行校驗(yàn)囚衔,準(zhǔn)備,解析和初始化雕沿,最終就會形成可以被虛擬機(jī)使用的java類型练湿,這就是一個虛擬機(jī)的類加載機(jī)制。java在類中的類是動態(tài)加載的审轮,只有在運(yùn)行期間使用到該類的時候肥哎,才會將該類加載到內(nèi)存中,java依賴于運(yùn)行期動態(tài)加載和動態(tài)鏈接來實(shí)現(xiàn)類的動態(tài)使用疾渣。

一個類的生命周期:

Paste_Image.png

加載贤姆,驗(yàn)證,準(zhǔn)備稳衬,初始化和卸載在開始的順序上是固定的,但是可以交叉進(jìn)行坐漏。
在Java中薄疚,對于類有且僅有四種情況會對類進(jìn)行“初始化”。

  • 使用new關(guān)鍵字實(shí)例化對象的時候赊琳,讀取或設(shè)置一個類的靜態(tài)字段時候(除final修飾的static外)街夭,調(diào)用類的靜態(tài)方法時候,都只會初始化該靜態(tài)字段或者靜態(tài)方法所定義的類躏筏。
  • 使用reflect包對類進(jìn)行反射調(diào)用的時候板丽,如果類沒有進(jìn)行初始化,則先要初始化該類趁尼。
  • 當(dāng)初始化一個類的時候埃碱,如果其父類沒有初始化過,則先要觸發(fā)其父類初始化酥泞。
  • 虛擬機(jī)啟動的時候砚殿,會初始化一個有main方法的主類

注意

  • 子類引用父類靜態(tài)字段芝囤,只會初始化父類不會初始化子類
  • 通過數(shù)組定義來引用類似炎,也不會觸發(fā)該類的初始化
  • 常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上沒有直接引用到定義常量的類悯姊,因此也不會觸發(fā)定義常量的類的初始化

類加載過程

加載

加載階段主要完成三件事羡藐,即通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流,將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)悯许,在Java堆中生成一個代表此類的Class對象仆嗦,作為訪問方法區(qū)這些數(shù)據(jù)的入口。這個加載過程主要就是靠類加載器實(shí)現(xiàn)的岸晦,這個過程可以由用戶自定義類的加載過程欧啤。

驗(yàn)證
這個階段目的在于確保才class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求睛藻,不會危害虛擬機(jī)自身安全。
主要包括四種驗(yàn)證:

  • 文件格式驗(yàn)證:基于字節(jié)流驗(yàn)證邢隧,驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范店印,并且能被當(dāng)前虛擬機(jī)處理。
  • 元數(shù)據(jù)驗(yàn)證:基于方法區(qū)的存儲結(jié)構(gòu)驗(yàn)證倒慧,對字節(jié)碼描述信息進(jìn)行語義驗(yàn)證按摘。
  • 字節(jié)碼驗(yàn)證:基于方法區(qū)的存儲結(jié)構(gòu)驗(yàn)證,進(jìn)行數(shù)據(jù)流和控制流的驗(yàn)證纫谅。
  • 符號引用驗(yàn)證:基于方法區(qū)的存儲結(jié)構(gòu)驗(yàn)證炫贤,發(fā)生在解析中,是否可以將符號引用成功解析為直接引用付秕。

準(zhǔn)備
僅僅為類變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類變量的初始值即零值兰珍,這里不包含用final修飾的static,因?yàn)?code>final在編譯的時候就會分配了(編譯器的優(yōu)化)询吴,同時這里也不會為實(shí)例變量分配初始化掠河。類變量會分配在方法區(qū)中,而實(shí)例變量是會隨著對象一起分配到Java堆中猛计。

解析
解析主要就是將常量池中的符號引用替換為直接引用的過程唠摹。符號引用就是一組符號來描述目標(biāo),可以是任何字面量奉瘤,而直接引用就是直接指向目標(biāo)的指針勾拉、相對偏移量或一個間接定位到目標(biāo)的句柄。有類或接口的解析盗温,字段解析藕赞,類方法解析,接口方法解析卖局。

初始化
初始化階段依舊是初始化類變量和其他資源主巍,這里將執(zhí)行用戶的static字段和靜態(tài)語句塊的賦值操作瘤袖。這個過程就是執(zhí)行類構(gòu)造器< clinit >方法的過程态秧。
< clinit >方法是由編譯器收集類中所有類變量的賦值動作和靜態(tài)語句塊的語句生成的少办,類構(gòu)造器< clinit >方法與實(shí)例構(gòu)造器< init >方法不同,這里面不用顯示的調(diào)用父類的< clinit >方法蟹演,父類的< clinit >方法會自動先執(zhí)行于子類的< clinit >方法风钻。即父類定義的靜態(tài)語句塊和靜態(tài)字段都要優(yōu)先子類的變量賦值操作。

類加載器

類加載器的分類

  • 啟動類加載器(Bootstrap ClassLoader):主要負(fù)責(zé)加載<JAVA_HOME>\lib目錄中的'.'或是-Xbootclasspath參數(shù)指定的路徑中的酒请,并且可以被虛擬機(jī)識別(僅僅按照文件名識別的)的類庫到虛擬機(jī)內(nèi)存中骡技。它加載的是System.getProperty("sun.boot.class.path")所指定的路徑jar
  • 擴(kuò)展類加載器(Extension ClassLoader):主要負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫布朦。它加載的是
    System.getProperty("java.ext.dirs")所指定的路徑或jar囤萤。
  • 應(yīng)用程序類加載器(Application ClassLoader):也叫系統(tǒng)類加載器,主要負(fù)責(zé)加載ClassPath路徑上的類庫是趴,如果應(yīng)用程序沒有自定義自己類加載器涛舍,則這個就是默認(rèn)的類加載器。它加載的是System.getProperty("java.class.path")所指定的路徑jar唆途。

類加載器的特點(diǎn)

  • 運(yùn)行一個程序時富雅,總是由Application Loader(系統(tǒng)類加載器)開始加載指定的類。
  • 在加載類時肛搬,每個類加載器會將加載任務(wù)上交給其父没佑,如果其父找不到,再由自己去加載温赔。
  • Bootstrap Loader(啟動類加載器)是最頂級的類加載器了蛤奢,其父加載器為null

類加載器的雙親委派模型

類加載器雙親委派模型的工作過程是:如果一個類加載器收到一個類加載的請求陶贼,它首先將這個請求委派給父類加載器去完成远剩,每一個層次類加載器都是如此,則所有的類加載請求都會傳送到頂層的啟動類加載器骇窍,只有父加載器無法完成這個加載請求(即它的搜索范圍中沒有找到所要的類),子類才嘗試加載锥余。

使用雙親委派模型主要是兩個原因:

  • 可以避免重復(fù)加載腹纳,當(dāng)父類已經(jīng)加載了,則就子類不需再次加載驱犹;
  • 安全因素嘲恍,如果不用這種,則用戶可以隨意的自定義加載器來替代Java核心API雄驹,則就會帶來安全隱患佃牛。

下面是一個類加載器雙親委派模型,這里各個類加載器并不是繼承關(guān)系医舆,它們利用組合實(shí)現(xiàn)的父類與子類關(guān)系俘侠。


雙親委托模型

類加載的幾種方式

  • 命令行啟動應(yīng)用時候由JVM初始化加載,加載含有main的主類蔬将。
  • 通過Class.forName("Hello")方法動態(tài)加載類爷速,默認(rèn)會執(zhí)行初始化塊,這是因?yàn)?code>Class.forName("Hello")其實(shí)就是Class.forName("Hello"霞怀,true,CALLCLASS.getClassLoader())惫东,第二個參數(shù)就是類加載過程中的連接操作。如果指定了ClassLoader,則不會執(zhí)行初始化塊廉沮。
  • 通過ClassLoader.loadClass("Hello")方法動態(tài)加載類颓遏,不會執(zhí)行初始化塊,因?yàn)?code>loadClass方法有兩個參數(shù)滞时,用戶只是用第一個參數(shù)叁幢,第二個參數(shù)默認(rèn)為false,即不對該類進(jìn)行解析則就不會初始化漂洋。

類加載實(shí)例

當(dāng)在命令行下執(zhí)行:java HelloWorld(HelloWorld是含有main方法的類的Class文件)遥皂,JVM會將HelloWorld.class加載到內(nèi)存中,并在堆中形成一個Class的對象HelloWorld.class刽漂。

基本的加載流程如下:

  • 尋找jre目錄演训,尋找jvm.dll,并初始化JVM贝咙;
  • 產(chǎn)生一個Bootstrap Loader(啟動類加載器)样悟;
  • Bootstrap Loader,該加載器會加載它指定路徑下的Java核心API庭猩,并且再自動加載Extended Loader(標(biāo)準(zhǔn)擴(kuò)展類加載器)窟她,Extended Loader會加載指定路徑下的擴(kuò)展JavaAPI,并將其父Loader設(shè)為BootstrapLoader蔼水。
  • Bootstrap Loader也會同時自動加載AppClass Loader(系統(tǒng)類加載器)震糖,并將其父Loader設(shè)為ExtendedLoader
  • 最后由AppClass Loader加載CLASSPATH目錄下定義的類趴腋,HelloWorld類吊说。

創(chuàng)建自己的類加載器

Java應(yīng)用開發(fā)過程中,可能會需要創(chuàng)建應(yīng)用自己的類加載器优炬。典型的場景包括實(shí)現(xiàn)特定的Java字節(jié)代碼查找方式颁井、對字節(jié)代碼進(jìn)行加密/解密以及實(shí)現(xiàn)同名Java類的隔離等。創(chuàng)建自己的類加載器并不是一件復(fù)雜的事情蠢护,只需要繼承自java.lang.ClassLoader類并覆寫對應(yīng)的方法即可雅宾。 java.lang.ClassLoader中提供的方法有不少,下面介紹幾個創(chuàng)建類加載器時需要考慮的:

  • defineClass():這個方法用來完成從Java字節(jié)碼的字節(jié)數(shù)組到java.lang.Class的轉(zhuǎn)換葵硕。這個方法是不能被覆寫的眉抬,一般是用原生代碼來實(shí)現(xiàn)的。
  • findLoadedClass():這個方法用來根據(jù)名稱查找已經(jīng)加載過的Java類懈凹。一個類加載器不會重復(fù)加載同一名稱的類吐辙。
  • findClass():這個方法用來根據(jù)名稱查找并加載Java類
  • loadClass():這個方法用來根據(jù)名稱加載Java類蘸劈。
  • resolveClass():這個方法用來鏈接一個Java類昏苏。

這里比較 容易混淆的是findClass()方法和loadClass()方法的作用。前面提到過,在Java類的鏈接過程中贤惯,會需要對Java類進(jìn)行解析洼专,而解析可能會導(dǎo)致當(dāng)前Java類所引用的其它Java類被加載。在這個時候孵构,JVM就是通過調(diào)用當(dāng)前類的定義類加載器的loadClass()方法來加載其它類的屁商。findClass()方法則是應(yīng)用創(chuàng)建的類加載器的擴(kuò)展點(diǎn)。應(yīng)用自己的類加載器應(yīng)該覆寫findClass()方法來添加自定義的類加載邏輯颈墅。 loadClass()方法的默認(rèn)實(shí)現(xiàn)會負(fù)責(zé)調(diào)用findClass()方法蜡镶。
前面提到,類加載器的代理模式默認(rèn)使用的是父類優(yōu)先的策略恤筛。這個策略的實(shí)現(xiàn)是封裝在loadClass()方法中的官还。如果希望修改此策略,就需要覆寫loadClass()方法毒坛。

下面的代碼給出了自定義的類加載的常見實(shí)現(xiàn)模式:

public class MyClassLoader extends ClassLoader {   
   protected Class<?> findClass(String name) throws ClassNotFoundException {       
      byte[] b = null; //查找或生成Java類的字節(jié)代碼       
      return defineClass(name, b, 0, b.length);   
   }
}

Java垃圾回收機(jī)制

Java堆內(nèi)存

分代收集

新生代(Young Generation)

  • Eden空間(Eden space望伦,任何實(shí)例都通過Eden空間進(jìn)入運(yùn)行時內(nèi)存區(qū)域)
  • S0 Survivor空間(S0 Survivor space,存在時間長的實(shí)例將會從Eden空間移動到S0 Survivor空間)
  • S1 Survivor空間 (存在時間更長的實(shí)例將會從S0 Survivor空間移動到S1 Survivor空間)

老年代(Old Generation)實(shí)例將從S1提升到Tenured(終身代)
永久代(Permanent Generation)包含類煎殷、方法等細(xì)節(jié)的元信息

enter image description here

永久代空間在Java SE8特性中已經(jīng)被移除屯伞。

垃圾回收過程

enter image description here

年輕代:使用標(biāo)記復(fù)制清理算法,解決內(nèi)存碎片問題豪直。因?yàn)樵谀贻p代會有大量的內(nèi)存需要回收劣摇,GC比較頻繁。通過這種方式來處理內(nèi)存碎片化弓乙,然后在老年代中通過標(biāo)記清理算法來回收內(nèi)存末融,因?yàn)樵诶夏甏枰换厥盏膬?nèi)存比較少,提高效率唆貌。
Eden 區(qū):當(dāng)一個實(shí)例被創(chuàng)建了,首先會被存儲在堆內(nèi)存年輕代的 Eden 區(qū)中垢乙。

Survivor 區(qū)(S0 和 S1):作為年輕代 GC(Minor GC)周期的一部分锨咙,存活的對象(仍然被引用的)從 Eden區(qū)被移動到 Survivor 區(qū)的 S0 中。類似的追逮,垃圾回收器會掃描 S0 然后將存活的實(shí)例移動到 S1 中酪刀。總會有一個空的survivor區(qū)钮孵。

老年代: 老年代(Old or tenured generation)是堆內(nèi)存中的第二塊邏輯區(qū)骂倘。當(dāng)垃圾回收器執(zhí)行 Minor GC 周期時(對象年齡計(jì)數(shù)器),在 S1 Survivor 區(qū)中的存活實(shí)例將會被晉升到老年代巴席,而未被引用的對象被標(biāo)記為回收历涝。老年代是實(shí)例生命周期的最后階段。Major GC 掃描老年代的垃圾回收過程。如果實(shí)例不再被引用荧库,那么它們會被標(biāo)記為回收堰塌,否則它們會繼續(xù)留在老年代中。

內(nèi)存碎片:一旦實(shí)例從堆內(nèi)存中被刪除分衫,其位置就會變空并且可用于未來實(shí)例的分配场刑。這些空出的空間將會使整個內(nèi)存區(qū)域碎片化。為了實(shí)例的快速分配蚪战,需要進(jìn)行碎片整理牵现。基于垃圾回收器的不同選擇邀桑,回收的內(nèi)存區(qū)域要么被不停地被整理瞎疼,要么在一個單獨(dú)的GC進(jìn)程中完成。

根可達(dá)性算法

Java語言規(guī)范沒有明確地說明JVM使用哪種垃圾回收算法概漱,但是任何一種垃圾收集算法一般要做2件基本的事情:

  • 發(fā)現(xiàn)無用信息對象
  • 回收被無用對象占用的內(nèi)存空間丑慎,使該空間可被程序再次使用。

GC Roots
根集就是正在執(zhí)行的Java程序可以訪問的引用變量的集合(包括局部變量瓤摧、參數(shù)竿裂、類變量)

GC Roots的對象包括

  • 虛擬機(jī)棧中所引用的對象(本地變量表)
  • 方法區(qū)中類靜態(tài)屬性引用的對象
  • 方法區(qū)中常量引用的對象
  • 本地方法棧中JNI引用的對象(Native對象)

**可達(dá)性算法分析 **

通過一系列稱為”GC Roots”的對象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索照弥,搜索所有走過的路徑稱為引用鏈腻异,當(dāng)一個對象到GC Roots沒有任何引用鏈相連時(從GC Roots到此對象不可達(dá)),則證明此對象是不可用的这揣,應(yīng)該被回收悔常。

根搜索算法:計(jì)算可達(dá)性,如圖:

根搜索算法

垃圾回收算法

引用計(jì)數(shù)法

引用計(jì)數(shù)法是唯一沒有使用根集(GC Roots)的垃圾回收的法给赞,該算法使用引用計(jì)數(shù)器來區(qū)分存活對象和不再使用的對象机打。堆中的每個對象對應(yīng)一個引用計(jì)數(shù)器。當(dāng)每一次創(chuàng)建一個對象并賦給一個變量時片迅,引用計(jì)數(shù)器置為1残邀。當(dāng)對象被賦給任意變量時,引用計(jì)數(shù)器每次加1柑蛇,當(dāng)對象出了作用域后(該對象丟棄不再使用)芥挣,引用計(jì)數(shù)器減1,一旦引用計(jì)數(shù)器為0耻台,對象就滿足了垃圾收集的條件空免。
唯一沒有使用根可達(dá)性算法的垃圾回收算法。
缺陷:不能解決循環(huán)引用的回收盆耽。

tracing算法(tracing collector)

tracing算法是為了解決引用計(jì)數(shù)法的問題而提出蹋砚,它使用了根集(GC Roots)概念扼菠。垃圾收集器從根集開始掃描,識別出哪些對象可達(dá)都弹,哪些對象不可達(dá)娇豫,并用某種方式標(biāo)記可達(dá)對象,例如對每個可達(dá)對象設(shè)置一個或多個位畅厢。在掃描識別過程中冯痢,基于tracing算法的垃圾收集也稱為標(biāo)記和清除(mark-and-sweep)垃圾收集器

compacting算法(Compacting Collector)

為了解決堆碎片問題框杜,在清除的過程中浦楣,算法將所有的對象移到堆的一端,堆的另一端就變成了一個相鄰的空閑內(nèi)存區(qū)咪辱,收集器會對它移動的所有對象的所有引用進(jìn)行更新振劳,使得這些引用在新的位置能識別原來的對象。在基于Compacting算法的收集器的實(shí)現(xiàn)中油狂,一般增加句柄和句柄表历恐。

copying算法(Coping Collector)

該算法的提出是為了克服句柄的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個對象面和多個空閑面专筷,程序從對象面為對象分配空間弱贼,當(dāng)對象滿了,基于coping算法的垃圾收集就從根集中掃描活動對象磷蛹,并將每個活動對象復(fù)制到空閑面(使得活動對象所占的內(nèi)存之間沒有空閑洞)吮旅,這樣空閑面變成了對象面,原來的對象面變成了空閑面味咳,程序會在新的對象面中分配內(nèi)存庇勃。

generation算法(Generational Collector) :現(xiàn)在的java內(nèi)存分區(qū)

stop-and-copy垃圾收集器的一個缺陷是收集器必須復(fù)制所有的活動對象,這增加了程序等待時間槽驶,這是coping算法低效的原因责嚷。在程序設(shè)計(jì)中有這樣的規(guī)律:多數(shù)對象存在的時間比較短,少數(shù)的存在時間比較長掂铐。因此罕拂,generation算法將堆分成兩個或多個,每個子堆作為對象的一代 (generation)堡纬。由于多數(shù)對象存在的時間比較短聂受,隨著程序丟棄不使用的對象蒿秦,垃圾收集器將從最年輕的子堆中收集這些對象烤镐。在分代式的垃圾收集器運(yùn)行后,上次運(yùn)行存活下來的對象移到下一最高代的子堆中棍鳖,由于老一代的子堆不會經(jīng)常被回收炮叶,因而節(jié)省了時間碗旅。

adaptive算法(Adaptive Collector)

在特定的情況下,一些垃圾收集算法會優(yōu)于其它算法镜悉∷畋伲基于Adaptive算法的垃圾收集器就是監(jiān)控當(dāng)前堆的使用情況,并將選擇適當(dāng)算法的垃圾收集器侣肄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旧困,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子稼锅,更是在濱河造成了極大的恐慌吼具,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矩距,死亡現(xiàn)場離奇詭異拗盒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锥债,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門陡蝇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哮肚,你說我怎么就攤上這事登夫。” “怎么了绽左?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵悼嫉,是天一觀的道長。 經(jīng)常有香客問我拼窥,道長戏蔑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任鲁纠,我火速辦了婚禮总棵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘改含。我一直安慰自己情龄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布捍壤。 她就那樣靜靜地躺著骤视,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鹃觉。 梳的紋絲不亂的頭發(fā)上专酗,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音盗扇,去河邊找鬼祷肯。 笑死沉填,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的佑笋。 我是一名探鬼主播翼闹,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蒋纬!你這毒婦竟也來了猎荠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤蜀备,失蹤者是張志新(化名)和其女友劉穎法牲,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琼掠,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拒垃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓷蛙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悼瓮。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖艰猬,靈堂內(nèi)的尸體忽然破棺而出横堡,到底是詐尸還是另有隱情,我是刑警寧澤冠桃,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布命贴,位于F島的核電站,受9級特大地震影響食听,放射性物質(zhì)發(fā)生泄漏胸蛛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一樱报、第九天 我趴在偏房一處隱蔽的房頂上張望葬项。 院中可真熱鬧,春花似錦迹蛤、人聲如沸民珍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嚷量。三九已至,卻和暖如春逆趣,著一層夾襖步出監(jiān)牢的瞬間蝶溶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工汗贫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留身坐,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓落包,卻偏偏與公主長得像部蛇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咐蝇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法涯鲁,類相關(guān)的語法,內(nèi)部類的語法有序,繼承相關(guān)的語法抹腿,異常的語法,線程的語...
    子非魚_t_閱讀 31,581評論 18 399
  • 從三月份找實(shí)習(xí)到現(xiàn)在旭寿,面了一些公司警绩,掛了不少,但最終還是拿到小米盅称、百度肩祥、阿里、京東缩膝、新浪混狠、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,184評論 11 349
  • (一)Java部分 1疾层、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨(dú)云閱讀 7,071評論 0 62
  • 《深入理解Java虛擬機(jī)》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分将饺。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,062評論 1 34
  • 前幾天晚上跟幾個投資人朋友聚會,聊到讀書這件事情的意義痛黎,大家說到能力成長水平主要差異是因?yàn)樽x書少予弧,我認(rèn)為有比讀書更...
    逄格亮閱讀 186評論 0 1