【转】深入理解JVM
http://www.cnblogs.com/liupeizhi/articles/1942764.html
1.類的加載:
在java中可分為四類加載器
引導類加載器 Bootstrap Loader 用來加載%java_home%lib下的核心類庫像String、Date等等
擴展類加載器 Extension Loader 用來加載%java_home%lib/ext下的擴展api
系統類加載器 AppClassLoader 加載classpath下面的類文件,我們的所有類文件默認都是由它來加載的,怎么樣,感覺親切吧
用戶自定義的類加載器
下面我們舉例說明類的加載
public class A{
public static void main(String args[]){
B b=new B();
}
}
Public class B{
Public static String a="hello";
public static String b=getValue();
Static{
System.out.println("Hello World");
}
Public static void getValue(){
}
}
假如我們自己定義了一個名字為A 的類,當我們啟動虛擬機即(java A)的時候,會創建一個JVM實例。現在我們就看類B 的加載過程。由于B是被A 引用 的,所以B是由A的類加載器進行加載。在這里我們不得不說一下類加載器是比較孝順的孩子,為什么這么說呢,因為類加載器在加載類的時候采用雙親委托機制。簡單說就是在加載類的時候,加載器會調用父類加載器來加載,父類再調用父類,依次類推。這個說起來比較抽象,我們這里給出源代碼來表示一下其中的加載過程:
public Class loadClass(String name){
ClassLoader parent=this.getClassLoader().getParent();
Try{
Class c=findLoadedClass(name);
//如果這個類沒有被加載
If(c!=null){
//如果有父類加載器
If(parent!=null)
parent.loadClass(name);
Else
BootstrapLoader.loadClass(name)
}catch(FileNotFoundException ex){
//如果父類加載器找不到,就調用自己的findClass查找
this.findClass(name);
}
//如果這個類已經被加載
Else
Return c;
}
這代碼是我自己寫的,是對源代碼的簡化表示,不要直接拷貝使用,如果想要知道詳細內容,建議參源碼。這段可以完全清晰地表示出類加載器的調用關系了。但是里面有個問題,相信各位都會發現了,就是 BootstrapLoader.loadClass(name).BootstrapLoader為什么不創建實例呢?因為BootstrapLoader并不是用java寫的,是一個本地方法(native),也就是說是用c/c++或者其他語言編寫的方法。為什么要這么做呢?主要是因為我們的類加載器也是類,如果他們都是用java實現,那么他們如何加載?所以,sun給了我們一個引導類加載器用來加載其他的類加載器,之后我們才能用這些類加載器加載我們的類文件。這里我們說一下他們的父子關系。
我們自定義的類加載器的父類加載器是 AppClassLoader
AppClassLoader的父類加載器是Extension Loader
Extension Loader的父類加載器是 Bootstrap Loader
當我們加載類B的時候,由于沒有指定它的類加載器,默認由AppClassLoader進行加載,調用loadClass()方法,AppClassLoader發現它的parent不是null,就會調用父類加載器(Extension Loader)加載,Extension Loader發現它的父母是null(因為BootstrapLoader 不是java寫的,所以不會被Extension Loader訪問到)于是就調用BootstrapLoader來加載,由于我們的B類是在我們的classpath中,所以必然會產生ClassNotFoundException ,接著調用自己的findClass進行查找,ExtensionLoader訪問的是%java_home%/lib/ext下面的類,必然也無法找到我們的B。于是會在AppClassLoader中捕獲到異常,然后接著調用AppClassLoader的findClass進行加載,結果找到了。
終于啊,經過這么復雜的遞歸調用和冒泡查找后找到了我們的類B 了。至于為什么要設計的這么復雜,直接加載不就完了嗎,干嘛搞得這么難受。這主要是出于安全性的考慮。你想想,這個過程中總是由Bootstrap來加載核心類,假如你自己寫了一個名字叫String的類,里面含有攻擊性的代碼,如果能加載成功,必然會導致其他依賴此類的類導致錯誤,整個JVM就會崩潰。然而這個類是無法加載到內存中的,因為類的加載總是由BootstrapLoader開始,當他發現已經加載了String,就不會再加載了,有效地保證了系統的安全性。類的加載過程基本就這樣,下面貼出一段代碼,自己實現的類加載器。
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
private String path="F:\\JSP\\ClassLoaderDemo\\classes\\";
private String fileType=".class";
@Override
public Class findClass(String name){
byte bytes[]=this.loadClassData(name);
return this.defineClass(name, bytes, 0, bytes.length);
}
//加載類數據,返回一個byte數組
public byte[] loadClassData(String name){
try {
FileInputStream fin=new FileInputStream(path+name+fileType);
ByteArrayOutputStream bout=new ByteArrayOutputStream();
int ch=0;
while((ch=fin.read())!=-1){
bout.write(ch);
}
return bout.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
//類加載器的測試類
public class TestLoader {
public static void main(String argsp[]){
MyClassLoader loader=new MyClassLoader();
try {
//在指定的目錄中加載HelloWorld.class文件
Class myclass=loader.loadClass("HelloWorld");
//加載完畢后進行實例化,這個過程包含了對類的解析
myclass.newInstance();
System.out.println(myclass.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
這段代碼可以直接拷貝運行.
我們大篇幅的講述了類的加載過程,這是jvm運行的第一步,建議各位讀到這里的時候在腦海中回顧一下類的整個加載過程,以便于理解下面我們要說的類的鏈接。如果你覺得理解的沒有問題,我們繼續說一下類的連接階段。
當我們將類加載到內存中的時候,我們就需要對他進行驗證。這里我們先介紹一下JVM是如何進行虛擬的。
想必大家都知道我們的CPU是由控制器,運算器,存儲器,輸入輸出設備組成的,這些是我們程序運行所必需的硬件環境。你可以這樣認為,我們的JVM為我們的java程序虛擬出了完整的一套運行環境,他的控制器由我們的JVM直接擔任,像垃圾處理了內存分配了等等,運算器當然就是我們的cpu了,存儲器是jvm的運行數據區,輸入輸出設備就是硬件設備了。這里你可以發現,我們的java是不能直接與硬件進行交互的,底層功能的實現需要通過本地方法進行實現,這就是我們的java跨平臺的原因。我們的JVM會根據硬件環境的不同(這里主要是指CPU的指令集的不同),將我們的class文件解釋成cpu可以識別的指令碼,這樣,我們的CPU就能夠運行我們的java程序了,這就是java的偉大之處。更確切的說使我們JVM的偉大之處了,呵呵。
這里,你只需要大概的了解一下JVM的原理就OK了,之后我們會細細的講解。
我們現在再說說類的連接階段。
當我們把類加載到內存之后,我們如何保證他的正確性呢,或者說我們如何保證加載進來的二進制碼是不是符合我們的Class類型的結構呢?關于結構,要細細說來需要很大的篇幅,這里你只需要這樣理解他Class就像是類的模板一樣,它包含類的所有信息,包括訪問控制符號(public,private,友好型)、是類還是接口,直接父類是誰,實現的接口有什么,以及字段信息,方法信息 ,還有一個常量池。你看看,這不就是我們類所包含的所有信息嗎。他們按照一定的結構組織在內存中,我們把這樣的一塊內存結構稱為Class。就是我們常說的類類型。
我們接著說,為了保證我們加載的二進制代碼是Class結構,所以我們需要進行校驗,很多地方稱為驗證,我感覺稱之為校驗更為合適。我們的校驗程序校驗完畢,發現它是我們需要的Class結構的時候,就會通知JVM為我們Class在方法區域分配空間。
說道這里,我們又要說說我們JVM的運行時數據區了,也就是他的存儲結構,下面,我會用圖示的方法來解釋它。
我們的JVM將運行數據區分為如下幾塊
堆:用來存儲我們創建的對象的地方
棧:JVM會為每個線程創建一個棧,用來存儲局部變量和操作數,棧跟棧之間不能通信。存儲單位是棧幀。我們每調用一個方法,就新建一個棧幀壓入棧中。棧幀之間是屏蔽的,這就是為什么一個方法無法訪問另一個方法中的變量。棧幀由局部變量區(用數組實現),操作數棧(棧結構),幀數據(主要用來支持對類常量池的解析,方法的正常返回,異常處理)
方法區:用來保存Class類型數據
JVM的內存主要結構就這么多了。
好了,我們接著說,也許你現在對這張圖還有很多疑問,稍后你就會明白了。我們的類現在已經通過驗證了,校驗器告訴我們它符合我們的Class結構,而且在方法區域為他分配了空間,我們非常高興。下面就是關乎初始化問題了。有人會問,不對還有解析呢。呵呵,在寫程序我們也知道了,這個階段是可選的,也就是說你可以讓你的類加載后馬上初始化,也可以加載完畢不進行初始化。在這里我們要讓我們的類初始化,下面即使解析階段了。
解析階段的主要任務:將類變量的符號引用解析成直接地址引用。就拿我們的變量a來說,他的值是"hello",這個東西在Class中只是一個符號而已。然而,我們的Class需要將所有的常量都存放在常量池中,所以hello會被存儲到常量池中,然后提供一個入口地址給a。a 就能直接引用它了。這里得好好地理解理解。
我們的方法b引用的是一個方法,這個方法在Class中只是一個符號而已,這個方法的實際代碼存放在一張表中,這張表我們成為方法表。我們的b就指向了方法表的一個引用。
解析完畢之后,就要初始化了,初始化很簡單,就是執行靜態代碼塊中的內容了。整個加載到此已經完畢,想必大家已經很清楚了吧。
然而,我們的JVM的任務才剛剛開始。
下面我們說一下對象的創建吧,想必這個問題在很多人看來都是很不解的。那么我們馬上開始吧。
對象的實例是什么呢?在內存中的樣子是什么呢。
如果你知道了方法區中的東西,對于對象也就不難理解了。對象就像一種結構,其中存儲了指向方法的引用,實例變量,一個指向類常量池的引用(這就是為什么實例可以訪問類變量和類方法)。這些數據按照一定的結構(就像Class結構一樣,只是簡單很多)存儲在我們的堆區,這就是我們耳熟能詳的對象。當我們new的時候,JVM就會按照上面的過程,在堆區為我們構造一個這樣的數據結構,然后將塊數據的引用存儲到棧里面。稍后我們會細細講解棧的結構
說到堆,我們不得不說JVM的內存管理機制,或者堆空間的垃圾處理機制。假如你自己寫了一個JVM,你肯定會碰到這樣一個問題,我們不斷的在堆里面創建對象,再大的內存也有耗盡的時候,那我們如何進行垃圾處理呢。以前,在JDK1的時候采用的是對整個堆空間進行掃描,查找不再被使用的對象將其回收,可想而知這種策略是多么的低效。后來,我們聰明的java工程師給我們提供了這樣的存儲結構,他們將堆分為了兩大部分,新生區和永久區。
總結
以上是生活随笔為你收集整理的【转】深入理解JVM的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jsmind
- 下一篇: 高通Sensor驱动学习笔记