java 測試開發基礎知識(類加載,JVM等)
寫在開頭:
面試的時候別人很可能會問你的java原理,.class load 原理, jvm機制,這些都是Java的底層知識,特整理如下:
1. 首先,編寫一個java程序,大家會用ide編寫一個例如helloworld.java的文件, 程序是能夠識別這個文件的,但是計算機不行,所以需要一個編譯的過程:
執行java.exe , 例如 在cmd的窗口執行: $java helloworld.java , 這個時候你會發現在同級目錄系生成了一個helloworld.class的可執行文件,這個文件是可以
被JVM直接執行的,原理如下:
.class文件大體如下:
JVM虛擬機只能識別.class文件這種字節碼文件,然後將字節碼翻譯成可執行的0,1;這個說明了JVM具有語言無關性,不僅僅是平臺無關性,這也是scala,Grovvy,JRUby….能在JVM上運行的原因,各種語言通過不同的編譯器將其編譯成.class文件
Java類加載機制(jvm 如何處理.class文件?)
首先要了解jvm的構造:
大多數 JVM 將內存區域劃分為 Method Area(Non-Heap)(方法區) ,Heap(堆) , Program Counter Register(程序計數器) , VM Stack(虛擬機棧,也有翻譯成JAVA 方法棧的),Native Method Stack ( 本地方法棧 ),其中Method Area 和 Heap 是線程共享的 ,VM Stack,Native Method Stack 和Program Counter Register 是非線程共享的。為什麽分為 線程共享和非線程共享的呢?請繼續往下看。
概括地說來,JVM初始運行的時候都會分配好 Method Area(方法區) 和Heap(堆) ,而JVM 每遇到一個線程,就為其分配一個 Program Counter Register(程序計數器) , VM Stack(虛擬機棧)和Native Method Stack (本地方法棧), 當線程終止時,三者(虛擬機棧,本地方法棧和程序計數器)所占用的內存空間也會被釋放掉。這也是為什麽我把內存區域分為線程共享和非線程共享的原因,非線程共享的那三個區域的生命周期與所屬線程相同,而線程共享的區域與JAVA程序運行的生命周期相同,所以這也是系統垃圾回收的場所只發生在線程共享的區域(實際上對大部分虛擬機來說知發生在Heap上)的原因。
所以內存泄露一般也發生在heap 和method area區域
類加載過程:
1.概述
Class文件由類裝載器裝載後,在JVM中將形成一份描述Class結構的元信息對象,通過該元信息對象可以獲知Class的結構信息:如構造函數,屬性和方法等,Java允許用戶借由這個Class相關的元信息對象間接調用Class對象的功能。
虛擬機把描述類的數據從class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
2.工作機制
類裝載器就是尋找類的字節碼文件,並構造出類在JVM內部表示的對象組件。在Java中,類裝載器把一個類裝入JVM中,要經過以下步驟:
類加載過程類加載分為三個步驟:加載,連接,初始化;
如下圖 , 是一個類從加載到使用及卸載的全部生命周期,圖片來自參考資料;
加載
根據一個類的全限定名(如cn.edu.hdu.test.HelloWorld.class)來讀取此類的二進制字節流到JVM內部;
將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構(hotspot選擇將Class對象存儲在方法區中,Java虛擬機規範並沒有明確要求一定要存儲在方法區或堆區中)
轉換為一個與目標類型對應的java.lang.Class對象;
連接
驗證
驗證階段主要包括四個檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證;
準備
為類中的所有靜態變量分配內存空間,並為其設置一個初始值(由於還沒有產生對象,實例變量將不再此操作範圍內);
解析
將常量池中所有的符號引用轉為直接引用(得到類或者字段、方法在內存中的指針或者偏移量,以便直接調用該方法)。這個階段可以在初始化之後再執行。
初始化
在連接的準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員自己寫的邏輯去初始化類變量和其他資源,舉個例子如下:
public static int value1 = 5; public static int value2 = 6; static{ value2 = 66; }
在準備階段value1和value2都等於0;
在初始化階段value1和value2分別等於5和66;
- 所有類變量初始化語句和靜態代碼塊都會在編譯時被前端編譯器放在收集器裏頭,存放到一個特殊的方法中,這個方法就是<clinit>方法,即類/接口初始化方法,該方法只能在類加載的過程中由JVM調用;
- 編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量;
- 如果超類還沒有被初始化,那麽優先對超類初始化,但在<clinit>方法內部不會顯示調用超類的<clinit>方法,由JVM負責保證一個類的<clinit>方法執行之前,它的超類<clinit>方法已經被執行。
- JVM必須確保一個類在初始化的過程中,如果是多線程需要同時初始化它,僅僅只能允許其中一個線程對其執行初始化操作,其余線程必須等待,只有在活動線程執行完對類的初始化操作之後,才會通知正在等待的其他線程。(所以可以利用靜態內部類實現線程安全的單例模式)
- 如果一個類沒有聲明任何的類變量,也沒有靜態代碼塊,那麽可以沒有類<clinit>方法;
何時觸發初始化
- 為一個類型創建一個新的對象實例時(比如new、反射、序列化)
- 調用一個類型的靜態方法時(即在字節碼中執行invokestatic指令)
- 調用一個類型或接口的靜態字段,或者對這些靜態字段執行賦值操作時(即在字節碼中,執行getstatic或者putstatic指令),不過用final修飾的靜態字段除外,它被初始化為一個編譯時常量表達式
- 調用JavaAPI中的反射方法時(比如調用java.lang.Class中的方法,或者java.lang.reflect包中其他類的方法)
- 初始化一個類的派生類時(Java虛擬機規範明確要求初始化一個類時,它的超類必須提前完成初始化操作,接口例外)
- JVM啟動包含main方法的啟動類時。
初步了解這麽多夠面試用了,如果想要更詳細的,請參考大佬的分析:
https://www.cnblogs.com/aspirant/p/7200523.html
java 測試開發基礎知識(類加載,JVM等)