1. 程式人生 > >帶著新人看java虛擬機01

帶著新人看java虛擬機01

基礎知識 als catch 對象分配 jar包 dcl jdk 挖掘 技術分享

1.前言(基於JDK1.7)  

  最近想把一些java基礎的東西整理一下,但是又不知道從哪裏開始!想了好久,還是從最基本的jvm開始吧!這一節就簡單過一遍基礎知識,後面慢慢深入。。。

  水平有限,我自己也是很難把jvm將清楚的,我參考一本書《深入java虛擬機第二版》(版本比較老,其實很多大佬的博客都是參考的這本書的內容。。。),電子檔pdf文件鏈接:https://pan.baidu.com/s/1bxs4i0gnVpz7Lkjl2fxS9g 提取碼:n5ou ,有興趣的小夥伴可以自己下載自己好好看看;

  所謂jvm,又名java虛擬機。我們平常寫java程序的時候幾乎是感覺不到jvm的存在的,我們只需要根據java規範去編寫類,然後就可以運行程序了,當然只有我們程序出現bug了,我們才有可能在控制臺上看到一些jvm報錯的信息,比如內存溢出異常等。

  java之所以能夠跨平臺,就是因為jvm屏蔽了各個操作系統之間的差異,舉個形象的例子,我們手機要充電吧,但是充電的方式有很多種,你可以直接數據線插到插座充電,也可以用數據線插到電腦USB口充電,一個是電腦一個是插座,為什麽都能給手機充電呢?原因就是有數據線屏蔽了插座和電腦的差異,對於手機來說,它是看不到數據線另外一頭連接的是什麽設備,只知道有電通過數據線向自己傳過來就ok了,順便一提,這也是所謂的適配器的原理!

  開始之前首先要明確一點,每一個java程序運行就會創建一個jvm實例!比如我同時在eclipse中同時運行三個程序,那麽就會創建三個jvm實例,三個程序運行於自己的jvm中,互不幹擾,當程序運行完畢,那麽jvm也會銷毀。

2.簡單看看類加載過程

  大家知道一個類加載到jvm大概是經過了幾個步驟的吧!編譯成字節碼文件,加載,鏈接(驗證,準備,解析),初始化....,我就簡單的用下面這個圖一起看看;

技術分享圖片

  在這裏,我們重點看看字節碼文件到jvm這一段,為什麽字節碼文件能夠被加載到jvm中呢?類加載器又是什麽呢?加載的具體過程又是什麽呢?鏈接,初始化又具體的是在做些什麽事呢?Class對象又是什麽鬼?jvm中的具體結構又是什麽樣子的,各有什麽用處?假如執行一個類中的方法,在jvm中到底是什麽流程呢?等等很多問題

  這些問題有的是了解一點,有的是真不知道,反正就是迷迷糊糊的一個類就加載成功了,然後我們就能成功調用那些方法了,平常用起來很舒服,但是細細想來難道不覺得奇怪嗎?

  反正我最初看到jvm的時候,最想吐槽的一句話就是:瑪德,為什麽啊?我感覺我已經要化身成十萬個為什麽了,咳咳,不說廢話了,開始往後學吧!

  下面我大概說一下這些步驟到底是做了什麽事,有個大概的流程,然後我們慢慢的深入探究每一個步驟到底是幹了什麽事!

  

  2.1 編譯器編譯

    這個沒什麽好說的,由於java是靜態語言,在執行java程序之前會先把我們寫的java文件給轉化成特殊的二進制碼的形式,編譯器就是做這個轉化的工作的工具,而且在我們寫代碼的時候,還沒運行程序之前,就會報錯,在某處代碼下面會有紅線標識,做這個工作的就是編譯器,還有最重要的源文件中泛型,是會在編譯器編譯這個階段就會進行擦除,所以字節碼文件中是沒有任何泛型信息的;

    順便提一下動態語言,比如Python,我們寫一個python程序運行,是不需要進行編譯的,會讀取第一行源文件中代碼就運行這一行的代碼,然後讀取第二行代碼,運行第二行代碼...

  

  2.2 類加載器的分類和加載順序

    什麽是類加載器呢?我有一個很生動很形象的例子:假如字節碼文件是一個人,而jvm就是地府,你說人死了會怎麽進入地府呢?自己肯定找不到地府的位置,於是要讓黑白無常請你過去了,類加載器在這裏就是黑白無常!

    大概了解類加載器的用處之後,我們就隨意看看類加載器的種類和運行原理;

    順便提一下,我們還記得最開始配置的jdk環境變量吧!我的JAVA_HOME=D:\java\jdk1.7;

    話說大家知道jar包到底是什麽嗎?其實就是一種壓縮文件的格式,跟zip,gz等壓縮格式沒有多大區別,可以用360壓縮打開。。。

    進入正題,類加載器分為四種,啟動類加載器(Bootstrap ClassLoader):最頂級的類加載器,還是用C++寫的;在我們編寫java程序的時候,編譯器會自動的幫我們導入一下常用的jar包,用的就是這個類加載器,比如我們最熟悉的lang包下的Object,String,Integer等都是我們可以直接用的,而不需要我們手動導入;具體的會導入哪些jar包呢,這就需要我們配置環境變量JAVA_HOME,編譯器會去環境變量中找%JAVA_HOME%\jre\lib ,這下面所有jar包然後進行加載到內存中,註意不是加載在JVM中;而且出於安全考慮,啟動類加載器只加載包名為java、javax、sun等開頭的類

    擴展類加載器(Extension ClassLoader):父類加載器是啟動類加載器,java語言實現,負責加載%JAVA_HOME%\jre\lib\ext 路徑下的jar包,這個不會自動加載,只有在需要加載的時候才去加載。

    應用類加載器(Application ClassLoader):父類加載器是擴展類加載器,java語言實現,也可以叫做系統類加載器(System ClassLoader),這個類加載器主要是加載我們在寫項目時編寫的放在類路徑下的類,比如maven項目中src/main/java/所有類

    自定義類加載器:需要我們自己實現,當特殊情況下我們需要自定義類加載器,只需要實現ClassLoader接口,然後重寫findClass()方法,我們就能夠自己實現一個類加載器,而且自己實現類加載器之後可以去加載任何地方的類。假如我新建一個類放在F盤的隨便一個角落裏也可以指定類路徑去加載,有興趣的小夥伴可以去試試。

    不考慮自定義類加載器,可以看到,啟動、擴展、應用這三個加載器就像是爺爺,爸爸,兒子一樣的關系,所以要加載一個類的話,選用哪個類加載器呢?肯定是有什麽好吃的先讓兒子吃呀,然而兒子又很有孝心,會把到手的好吃的給爸爸吃。爸爸又會給爺爺吃,爺爺會嘗試著吃,假如一看這東西糖分太高於是就又給爸爸吃,爸爸也嘗試著吃,發現這東西不好吃,於是最後還是給兒子吃....這就是類加載器的雙親委托機制,隨便找了一幅圖看看:

技術分享圖片

  

  2.3.JVM內部結構

    其實大多數人對JVM是很熟悉了,不就是那幾個塊嗎?本地方法棧,java棧,java堆,方法區,pc計數器,我這裏就先大概說一下這幾個部分的用處;

技術分享圖片

    方法區:類加載器其實就是將字節碼文件給丟到這裏,並解析出字節碼文件中包含的一些信息,比如全類名,類變量,方法有關的信息,父類信息,是不是接口等等這類信息

    由於方法區很重要,我就隨意畫個草圖:

技術分享圖片

    常量池(屬於方法區):由於方法區比較厲害能把字節碼文件中很多信息給解析出來,但其中可能有很多常量比如18,“helloworld”,以及一些符號引用,常量池就存這些東西;但是什麽又是符號引用呢?我就大概說一下吧,假如兩個類Animal和Dog,在Animal類中有個方法裏面是這樣的:Dog dog = new Dog();dog.run(); 這個時候問題來了,在加載Animal類的時候發現了要用到Dog類,肯定是要去加載Dog類的,那麽有兩種做法,第一種先暫停Animal類的加載去加載Dog類,加載完之後再加載Dog類,第二種,Animal類繼續加載的同時順便加載Dog類,只是Animal中只要是用到了Dog類、方法、字段的所有地方我隨便用xxx來表示,等Dog類加載完之後我再把xxx指向方法區Dog類對應的地址就ok了;我們當然用第二種方法啦,並且在這裏我們隨便用的xxx就是符號引用,而加載完成後方法區中的Dog類地址就是直接引用

    java堆:根據方法區中存的這麽豐富的信息,這裏就會創建每一個類的Class對象,話說這個Class對象用的最多的就是反射,那麽這個Class對象到底是個什麽呢?其實不用想的太難理解了,你就把它看作字節碼文件在內存中的另外一種形式唄,就好像大米,在電飯煲裏的表現形式就是米飯,在高壓鍋裏的表現形式就是粥了.....;假如程序運行的話,還會在堆中創建對象並且存放在堆中,所有的同類型的類的實例對象共享一個Class對象,我也隨意畫了一個草圖來看看如下所示,所以同一個類的不同實例對象的xx.getClass()都是一樣的,而且根據獲得的Class對象可以利用反射創建新的對象和獲取其中的方法,可以說Class對象為我們程序員提供了一個操作堆中對象的一個安全通道

技術分享圖片

    

    pc寄存器:對於多線程來說,你就可以把這個看作一個計數器,每個線程一個,裏面寫著1,2,3,4,5....記錄著各個線程執行代碼的行號,為什麽要記這個行號呢?莫非是閑的蛋疼?當然不是!因為對於多線程來說,cpu首先執行一號線程,然後停止,去執行二號線程,又停止,又去執行一號線程...這個時候問題來了,cpu怎麽知道上一次一號線程執行到哪裏來了?於是啊,這個pc寄存器用處就來了,因為每個線程都有一個,而且記錄著當前執行的行號,下次cpu來了根據這個行號就可以接著執行了啊!

    java棧:對象已經創建完畢放在堆中,然後我們調用一個java方法,就會在java棧中開辟一小塊空間(就是所謂的壓棧),俗稱棧幀,棧幀可以有多個,因為一個方法中可以調用其他方法嘛!總之一個方法就對應一個棧幀,棧幀裏面放著我們這個要運行方法內的局部變量,方法返回值等等參數,等這個方法執行完之後這個棧幀就退出去了(這就是所謂的彈棧),然後棧就恢復原樣

    本地方法棧:不知道大家有沒有打開JDK的一些類的源碼看看,很多類都有Native方法(本地方法),我的理解是就是調用操作系統中一些c語言實現的方法或者其他語言實現的方法....

  2.4.加載

    說了這麽久的類加載器的種類還有類加載器的使用順序,然後也簡單說了JVM內部結構以及各自的作用,現在就是選好了的類加載器去加載字節碼文件丟到JVM中的方法區中了。

    用偽代碼隨便看看加載大概步驟,參數name就是我們傳進去的類的全名:

public Class<?> loadClass(String name)  {  
        try {  
            if (parent != null) {  
                //如果存在父類加載器,就委派給父類加載器加載  
                c = parent.loadClass(name, false);  
            } else {  
                //如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,  通過調用本地方法native findBootstrapClass0
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果父類加載器和啟動類加載器都不能完成加載任務,才調用自身的加載功能  
            c = findClass(name);  
        }  

  所以假如我自定義一個類加載器MyClassLoader,那麽就可以用這種方式去加載我隨意放在F盤myclass目錄裏面,com.wyq.test包下的一個Student類:

MyClassLoader myClassLoader=new MyClassLoader("F:\\myclass");
Class c=myClassLoader.loadClass("com.wyq.test.Student");

  然後我們得到了這個類的Class對象就可以用反射對這個類為所欲為了,嘿嘿嘿嘿~

  2.5.鏈接

    鏈接中分為三步:驗證,準備,解析;

    隨便說說這三步大概幹些什麽,驗證:這一步其實沒什麽大的用處,就是虛擬機會檢查一下我們的字節碼文件有沒有問題,具體的就是看看你字節碼文件格式有問題嗎?語法有沒有問題?等等

    準備:給類的靜態變量分配內存空間,並設置初始值;大家都知道靜態變量是放在方法區中的吧,比如我java類中有個靜態變量static int age = 18 那麽這這個階段首先會分配4個字節的內存空間,然後設置初始值為0,八大基本數據類型都有初始值,可以了解了解

    解析:比較專業一點的說法就是,在解析階段,JVM會把類的二進制數據中的符號引用替換為直接引用!這句話怎麽理解請看上面介紹的常量池

  

  2.6 初始化

    還是用準備階段那個靜態變量,根據字節碼文件,將準備那個階段的初始值覆蓋成真正的值18;

  順便說一句,加載、鏈接、初始化三個步驟不是一定要按照這個順序完成的,只是開始的順序是這個,但是在執行過程中可能會有彎道超車的現象

3.例子分析

  這裏我們寫一個最簡單的例子來總結一下上面這麽多知識;

public class Animal{
  private int age=18;
  public void run() {}  

}

publci class Test{
  public static void main(String[] args){
    Animal animal = new Animal();
    animal.run();

  }
}

  運行這個main方法的步驟:

  1.首先是編譯器會將這兩個類都編譯成字節碼文件並放在你的項目存放路徑

  2.Test這個類會以某種方式告訴JVM自己的類名“Test”,虛擬機就會以某種牛逼的方法可以找到你這個Test.class放在那個目錄下面

  3.調用類加載器,采用雙親委托機制去加載這個類,最後不出意外應該是應用類加載器去加載這個Test.class,以二進制流的形式加載進JVM方法區

  4.在加載之後會去驗證這個Test.class是否符合規範,沒問題的話就會解析這個加載進來的Test.class,將其中很多信息都保存下來,常量和符號引用保存在常量池中,其他的比如訪問修飾符,全類名,直接父類的全類名,方法和字段信息,除了常量以外的所有靜態變量,以及指向類加載器和Class對象的指針等都存在常量池外面

  5.通過保存在方法區中的字節碼,JVM可以執行main()方法,在執行這個方法的時候,會一直持有有一個指向Test的常量池的指針;

  6.在執行main方法的第一條指令的時候,就是告訴JVM為Test常量池的第一個類型分配足夠內存;由於main方法一直持有執行Test常量池指針於是很迅速的找到了常量池第一項,發現它是一個對Animal類的符號引用,然後就會先檢查方法區看有沒有Animal類有沒有被加載,假如沒有的話就要去找到這個Animal類;這裏就有了一個算法的小知識,怎麽才能夠讓虛擬機最快速度找到Animal類所在位置呢?可以用散列表,搜索樹等算法。

  7.加載Animal.class到方法區並提取其中有用的信息保存在方法區,然後替換Test常量池第一個類型的符號引用,變為直接引用;註意,這個時候還沒有創建對象,直接引用指向的是方法區中Animal所在的地址

  8.JVM在堆中為創建Animal對象分配足夠內存,怎麽確定這個內存多大合適呢?其實JVM比較牛,已經設好了可以根據方法區中存放的信息確定一個類創建對象要用到多少堆空間;

  9.對象創建好了會設置Animal實例變量的默認初始值:age = 0

  10.創建一個棧幀(裏面有一個指向Animal對象的引用),壓入java棧中,到此main方法第一條指令就執行完畢;還記得一個方法一個棧幀麽

  11.然後根據這個棧幀調用java代碼,將age的值初始化為正確的值:18

  12.通過這個棧幀執行run()方法,又會開辟一個棧幀存放run()方法內部的所有信息

  13.run()方法執行完畢,釋放這個棧幀;然後main()執行完畢,釋放棧幀;然後就是程序執行完畢,清理回收堆中所有對象以及方法區

  大概就是這麽一個流程,其中最後的那個清理回收過程其實很重要,由於java棧和方法區的清理內存效率非常好,我們可以不用在意,重點是在堆中清理內存,而且由於有的程序是會運行很久的,不可能每次都等程序執行完畢之後再一起清理,肯定是要一邊運行程序一邊清理堆內存中沒用的對象,那麽又該怎麽進行處理呢?又會涉及到很多的算法以及堆內部到底是什麽結構,後面我們會逐漸挖掘...

帶著新人看java虛擬機01