不知不覺從事開發快三年了,這三年自己學的也挺多,但是由於工作用不上,又忘了;最後發現,自己連一個Java類是怎樣運行的都不知道,於是拿起曾經的入門書本《瘋狂Java》,結合自己的理解,把停更一年多的博客寫起來_^_。
JVM和類
當調用Java命令運行某個Java程序時,該命令將會啓動一個java虛擬機進程,不管該java程序有多麼複雜,該程序啓動了多少個線程,它們都處於該java虛擬機進程裏。同一個JVM的所有線程,所有變量都處於同一個進程裏,它們都使用該JVM進程的內存區。當系統出現以下幾種情況時,JVM進程將被終止。
- 程序運行到最後正常結束
- 程序運行到使用System.exit()或Runtime.getRuntime().exit()代碼處結束程序
- 程序執行過程中遇到未捕獲的異常或錯誤而結束
- 程序所在平臺強制結束了JVM進程
當java程序運行結束時,JVM進程結束,該進程在內存中的狀態將會丟失。
下面看一段程序代碼:
public class A {
/**
* 定義一個類變量
*/
public static int value = 6;
}
public class A1 {
public static void main(String[] args) {
A.value ++;
// 7
System.out.println("A's value = " + A.value);
}
}
public class A2 {
public static void main(String[] args) {
// 6
System.out.println("A's value = " + A.value);
}
}
相信大家結果沒有異議,因爲A1和A2都有main()方法,運行了兩次JVM。第一次運行A1的main()方法結束後,JVM也結束了,它對A類所做的修改將全部丟失,A2運行main()方法時,JVM將再次初始化A類
我們再來看看這段代碼
public class A3 {
public static void main(String[] args) {
A.value ++;
// 7
System.out.println("A3 A's value = " + A.value);
A4 a4 = new A4();
a4.getValue();
}
}
public class A4 {
// 7
public void getValue(){
System.out.println("A4 A's value = " + A.value);
}
}
現在A3,A4中value的值是一樣的,因爲只有運行了一個main()方法,A3,A4訪問到的A的value處於同一個JVM進程中,值是一樣的。
類的加載
當程序主動使用某個類時,如果該類還未被加載到內存中,則系統會通過加載、連接、初始化三個步驟來對該類進行初始化。如果沒有意外,JVM將會連續完成這三個步驟,所以也將這三個步驟統稱爲類加載或類初始化。
類加載指的是將類的class文件讀入內存,併爲之創建一個java.lang.Class對象,也就是說,當程序中使用任何的類時,系統都會爲之建立一個java.lang.Class對象。系統中所有的類實際上也是實例,它們都是java.lang.Class的實例。
類的加載由類加載器完成,類加載通常由JVM提供,這些類加載器也是前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱爲系統類加載器。除此之外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。
通過使用不同的類加載器,可以從不同來源加載類的二進制數據,通常有如下幾種來源。
- 從本地文件系統加載class文件,這是前面絕大java程序的類加載方式
- 從JAR包加載class文件,這種方式也很常見。我們使用第三方jar包時,一般先下載下來,放在lib目錄下,如JDBC編程時用到的數據庫驅動類
- 通過網絡加載class文件
- 把一個java源文件動態編譯,並執行加載
類加載器通常無須等到“首次使用”該類時才加載該類,Java虛擬機規範允許系統預先加載某些類。
類的連接
當類被加載之後,系統爲之生成一個對應的Class對象,接着將會進入連接階段,連接階段負責把類的二進制數據合併到JRE中。類連接分爲三個 階段。 - 驗證:驗證階段用於檢驗被加載的類是否有正確的內部結構,並和其他類協調一致
- 準備:類準備階段則負責爲類的類變量(static)分配內存,並設置默認初始值
- 解析:將類的二進制數據中的符號引用替換爲直接引用
1.符號引用(Symbolic References):
符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class文件中它以CONSTANT_Class_info、
CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現。符號引用與虛擬機的內存佈局無關,引用的目標並不一定加載到內存中。在Java中,一個java類
將會編譯成一個class文件。在編譯時,java類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。
2.直接引用(Direct References):
直接引用可以是
(1)直接指向目標的指針(比如,指向“類型”【Class對象】、類變量、類方法的直接引用可能是指向方法區的指針)
(2)相對偏移量(比如,指向實例變量、實例方法的直接引用都是偏移量)
(3)一個能間接定位到目標的句柄
直接引用是和虛擬機的佈局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經被加載入內存中了。
類的初始化
在類的初始化階段,虛擬機負責對類進行初始化,主要就是對類變量進行初始化。在Java類中對類變量指定初始值有兩種方式:
- 聲明類變量時指定初始值
- 使用靜態初始化塊爲類變量指定初始值
public class ClassInit {
/**
* 聲明時指定初始值
*/
static int a = 1;
static int b;
static {
/**
* 使用靜態初始化塊指定初始值
*/
b = 2;
}
}
JVM初始化一個類包含如下幾個步驟:
-
假如這個類還沒有被加載和連接,則程序先加載並連接該類
-
假如該類的直接父類還沒有被初始化,則先初始化其直接父類(所以JVM最先初始化得總是java.lang.Object類)
-
假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化的時機
當Java程序首次通過下面6中方式來使用某個類或接口時,系統就會初始化該類或接口。 -
創建類的實例。爲某個類創建實例的方式包括:使用new操作符來創建實例,通過反射來創建實例,通過反序列化的方式來創建實例
public class B {
static {System.out.println("B is init……");}
}
public class C implements Serializable {
static {System.out.println("C is init……");}
}
public class Test {
public static void main(String[] args) throws Exception{
// 反射
Class<B> b = B.class;
// 輸出 B is init……
B b1 = b.newInstance();
/**
* 序列化,先將C類序列化後寫入c.txt
* 序列化的目的是將java對象轉換成二進制編碼,方便在網絡中傳輸,
* 如果爲了安全,怕類中的某個屬性值在網絡傳輸中被篡改或竊取,用transient修飾該屬性
*/
/*ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("c.txt"));
C c = new C();
outputStream.writeObject(c);*/
// 反序列化
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("c.txt"));
// 輸出 C is init……
C bankUp = (C)inputStream.readObject();
}
}
- 調用某個類的類方法(靜態方法)
- 訪問某個類或接口的類變量,或爲該類變量賦值
- 使用反射方式來強制創建某個類或接口對應的java.lang.Class對象。
public class D {
static {System.out.println("D is init……");}
}
public class Test {
public static void main(String[] args) throws Exception{
/**
* name要補全全路徑,否則會拋ClassNoFound異常
*/
Class<?> d = Class.forName("cn.crazy.reflect.D");
// 輸出 D is init……
D d1 = (D)d.newInstance();
}
}
- 初始化某個類的子類。當初始化某個類的子類時,該子類的所有父類都會被初始化
- 直接使用java.exe命令來運行某個主類,當運行某個主類時,程序會先初始化該主類
對應一個final型的類變量,如果該類變量的值在編譯時就可以確定下來,那麼這個類變量相當於“宏變量”。java編譯器會在編譯時直接把這個類變量出現的地方替換成它的值,因此即使程序使用該靜態類變量也不會導致類的初始化。
public class A {
static {System.out.println("A is init……");}
/**
* 定義一個類變量
*/
public static int value = 6;
public static final int finalValue = 1;
public static void test(){
System.out.println("A's test()");
}
}
public class Test {
public static void main(String[] args) throws Exception{
// 輸出 A's finalValue = 1
System.out.println("A's finalValue = " + A.finalValue);
/** 輸出:
* A is init……
* A's value = 6
*/
System.out.println("A's value = " + A.value);
}
}
public class Time {
static {
System.out.println("Time is init……");
}
static final String compileConstant = System.currentTimeMillis() + "";
}
public class TimeTest {
public static void main(String[] args) throws Exception{
ClassLoader loader = ClassLoader.getSystemClassLoader();
/**
* 加載,不會進行初始化
*/
Class<?> time = loader.loadClass("cn.crazy.reflect.Time");
/**
* 初始化
* 輸出 Time is init……
*/
Class.forName("cn.crazy.reflect.Time");
}
}
從運行結果可以看出,必須等到執行Class.forName(“cn.crazy.reflect.Time”)時才完成對Time類的初始化
類加載器
類加載器負責將.class文件(可能在磁盤上,也可能在網絡上)加載到內存中,併爲之生對應的java.lang.Class對象。
類加載器簡介
類加載器負責加載所有的類,系統爲所有被載入內存中的類生成一個java.lang.Class實例。一旦一個類被載入JVM中,同一個類就不會被再次載入了。
正如一個對象有一個唯一的標識一樣,一個載入JVM的類也有一個唯一的標識,在java中,一個類用器全限定類名(包含包名和類名)作爲標識;但在JVM中,一個類用其全限定類名和其類加載器作爲唯一標識。例如,如果在pg的包中有一個名爲Person的類,被類加載器ClassLoader的實例kl負責加載,則該Person類對應的Class對象在JVM中表示爲(Person、pg、kl)。這就意味着兩個類加載器加載的同名類:(Person、pg、kl)和(Person、pg、k2)是不同的、它們所加載的類也是完全不同、互不兼容的。
當JVM啓動時,會形成由三個類加載器組成的初始類加載器層次結構。
- Bootstrap ClassLoader:根類加載器
Boostrap ClassLoader被稱爲引導(也稱爲原始或根)類加載器,它負責加載java的核心類。在Sun的JVM中,當執行java.exe
命令時,使用-Xbootclasspath選項或使用-D選項指定sun.boot,class.path系統屬性值可以指定加載附加的類;
根加載器比較特殊,它並不是java.lang.ClassLoarder的子類,而是由JVM自身實現的,
public class BootstrapLoader {
public static void main(String[] args) {
/**
* 獲取根類加載器所加載的全部URL數組
*/
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
/**
* 遍歷輸出
*/
for (URL url : urLs) {
System.out.println("url = " + url);
}
}
}
輸出結果:
url = file:/D:/java/jre/lib/resources.jar
url = file:/D:/java/jre/lib/rt.jar
url = file:/D:/java/jre/lib/sunrsasign.jar
url = file:/D:/java/jre/lib/jsse.jar
url = file:/D:/java/jre/lib/jce.jar
url = file:/D:/java/jre/lib/charsets.jar
url = file:/D:/java/jre/lib/jfr.jar
url = file:/D:/java/jre/classes
程序中可以使用String、System這些核心類庫--因爲這些核心類庫都在D:\java\jre\lib\rt,jar
- Extension ClassLoader: 擴展類加載器
Extension ClassLoader被稱爲擴展類加載器,它負責加載JRE的擴展目錄(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系統屬性指定的目錄)
中JAR包的類
通過這種方式,就可以爲java擴展核心類以外的新功能,只要把自己開發的類打包成JAR文件,然後放在%JAVA_HOME%/jre/lib/ext路徑即可
- System ClassLoader:系統類加載器
System ClassLoader 被稱爲系統(也稱爲應用)類加載器,它負責在JVM啓動時加載來自java命令的-classpath選項、java.class.path系統屬性,
或CLASSPATH環境變量所指定的JAR包和路徑()。程序可以通過ClassLoader的靜態方法getSystemClassLoader()來獲取系統類加載器。如果沒有特別
指定,則用戶自定義的類加載器都以類加載器作爲父加載器。
類加載機制
JVM的類加載器機制主要有如下三種:
- 全盤負責。所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示指定使用另一個類加載器進行加載。
- 父類委託。所謂父類委託,則是先讓parent(父)類加載器試圖加載該Class,只有在父加載器無法加載該類時才嘗試從自己的類路徑中加載該類。
- 緩存機制。緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存中搜尋該Class,只有當緩存區不存在該Class對象時,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區中。這就是爲什麼修改Class後,必須重新啓動JVM。程序所做的修改纔會生效的原因。
除了可以使用Java提供的類加載器外,開發者可以自定義類加載器。自定義的類加載器通過繼承ClassLoader來實現。JVM中這4中類加載器的層次結構如下:
來看一段代碼:
public class SystemLoader {
public static void main(String[] args) throws Exception{
ClassLoader loader = ClassLoader.getSystemClassLoader();
System.out.println("系統類加載器: " + loader);
/**
* 獲取系統類加載器的加載路徑--通常由CLASSPATH環境變量指定
* 如果操作系統沒有指定CLASSPATH環境變量。則默認當前路徑作爲
* 系統類加載器的加載路徑(我沒有指定CLASSPATH)
* (我的項目放在F:/Java)
*/
Enumeration<URL> resources = loader.getResources("");
while (resources.hasMoreElements()){
System.out.println("resource = " + resources.nextElement());
}
/**
* 獲取系統類加載器的父類加載器
*/
ClassLoader extensionLoader = loader.getParent();
System.out.println("SystemClassLoader's parent is : " + extensionLoader);
/**
* 擴展類加載器的加載路徑
*/
System.out.println("擴展類加載器的加載路徑: " + System.getProperty("java.ext.dirs"));
/**
* 擴展類加載器的父類加載器
*/
System.out.println("args = " + extensionLoader.getParent());
}
}
輸出:
系統類加載器: sun.misc.Launcher$AppClassLoader@18b4aac2
resource = file:/F:/Java/out/production/Java/
SystemClassLoader's parent is : sun.misc.Launcher$ExtClassLoader@135fbaa4
擴展類加載器的加載路徑: D:\Java\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
args = null
我的項目路徑:
從上面的運行結果可以看出,系統類加載器的加載路徑是程序運行的當前路徑,擴展類加載器的加載路徑是%JAVA_HOME%/jre/lib/ext,此處看到擴展類加載器的父類加載器是null,並不是根加載器。這是因爲根加載器沒有繼承ClassLoader抽象類,所以擴展類加載器的getParent()返回null。但實際上,擴展類加載器的父類加載器是根類加載器,只是根類加載器不是Java實現的。
下面來看看類加載器的關係:
static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
void addExtURL(URL var1) {
super.addURL(var1);
}
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
private static URL[] getExtURLs(File[] var0) throws IOException {
Vector var1 = new Vector();
for(int var2 = 0; var2 < var0.length; ++var2) {
String[] var3 = var0[var2].list();
if (var3 != null) {
for(int var4 = 0; var4 < var3.length; ++var4) {
if (!var3[var4].equals("meta-index")) {
File var5 = new File(var0[var2], var3[var4]);
var1.add(Launcher.getFileURL(var5));
}
}
}
}
URL[] var6 = new URL[var1.size()];
var1.copyInto(var6);
return var6;
}
public String findLibrary(String var1) {
var1 = System.mapLibraryName(var1);
URL[] var2 = super.getURLs();
File var3 = null;
for(int var4 = 0; var4 < var2.length; ++var4) {
File var5 = (new File(var2[var4].getPath())).getParentFile();
if (var5 != null && !var5.equals(var3)) {
String var6 = VM.getSavedProperty("os.arch");
File var7;
if (var6 != null) {
var7 = new File(new File(var5, var6), var1);
if (var7.exists()) {
return var7.getAbsolutePath();
}
}
var7 = new File(var5, var1);
if (var7.exists()) {
return var7.getAbsolutePath();
}
}
var3 = var5;
}
return null;
}
private static AccessControlContext getContext(File[] var0) throws IOException {
PathPermissions var1 = new PathPermissions(var0);
ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);
AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});
return var3;
}
static {
ClassLoader.registerAsParallelCapable();
}
}
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}
protected PermissionCollection getPermissions(CodeSource var1) {
PermissionCollection var2 = super.getPermissions(var1);
var2.add(new RuntimePermission("exitVM"));
return var2;
}
private void appendToClassPathForInstrumentation(String var1) {
assert Thread.holdsLock(this);
super.addURL(Launcher.getFileURL(new File(var1)));
}
private static AccessControlContext getContext(File[] var0) throws MalformedURLException {
PathPermissions var1 = new PathPermissions(var0);
ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);
AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});
return var3;
}
static {
ClassLoader.registerAsParallelCapable();
}
}
AppClassLoader 類和ExtClassLoader類 都是Launcher類的靜態內部類。
從上面的結果可以看出,系統類加載器是AppClassLoader的實例,擴展類加載器是ExtClassLoader的實例,它們倆個都是URLClassLoader類的實例。
類加載器加載Class大致要經過以下8個步驟:
-
檢測此Class是否載入過(即在緩存區是否有此Class),如果有則直接進入第8步,否則接着執行第2步
-
如果父類加載器不存在(如果沒有父加載器,要麼父類加載器是根類加載器,要麼本身就是根加載器),則跳到第4步執行;如果父類加載器存在,則接着執行第3步
-
請求使用父類加載器去載入目標類,如果成功載入則跳到第8步,否則接着執行第5步
-
請求使用根類加載器來載入目標類,如果成功載入則跳到第8步,否則跳到第7步
-
當前類加載器嘗試尋找Class文件(從與此ClassLoader相關的類路徑中尋找),如果找到則執行第6步,如果找不到則跳到第7步
-
從文件中載入Class,成功載入後跳到第8步
-
拋出ClassNotFoundException異常
-
返回對應的java.lang.Class對象
其中,第5、6步允許重寫ClassLoader的findClass()方法來實現自己的載入策略,甚至重寫loadClass()方法來實現自己的載入過程。
自定義類加載器我在下了篇文章繼續描述。