JVM核心學習筆記

bboy楓亭發表於2020-10-02

1. 類載入全過程

為什麼研究類載入全過程?
– 有助於瞭解JVM執行過程
– 更深入瞭解 java 動態性,(解熱部署、動態載入),提高程式的靈活性。

類載入機制
– JVM 把 class檔案載入到記憶體,並對資料進行校驗、解析和初始化,最終形成 JVM 可以直接使用的 Java 型別的過程。·
在這裡插入圖片描述

1.1 載入

將 class 檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區中的執行時資料結構,在堆中生成一個代表這個類的 java.lang.Class 物件,作為方法區類資料的訪問入口。 這個過程需要類載入器參與。
在這裡插入圖片描述

1.2 連結

將Java類的二進位制程式碼合併到JVM的執行狀態之中的過程
(1)驗證:確保載入的類資訊符合JVM規範,沒有安全方面的問題。
(2)準備:正式為類變數(static變數)分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配
(3)解析:虛擬機器常量池內的符號引用替換為直接引用的過程

1.3 初始化

初始化階段是執行類構造器()方法的過程。類構造器()方法是由編譯器自動收集
類中的所有類變數的賦值動作和靜態語句塊(static塊)中的語句合併產生的。
• 當初始化一個類的時候,如果發現其父類還沒有進行過初始化、則需要先出發其父類的初始化
• 虛擬機器會保證一個類的()方法在多執行緒環境中被正確加鎖和同步。

在這裡插入圖片描述
類的主動引用(一定會發生類的初始化)
– new一個類的物件
– 呼叫類的靜態成員(除了final常量)和靜態方法
– 使用java.lang.reflect包的方法對類進行反射呼叫
– 當虛擬機器啟動,java Hello,則一定會初始化Hello類。說白了就是先啟動main方法所在的類
– 當初始化一個類,如果其父類沒有被初始化,則先會初始化他的父類
類的被動引用(不會發生類的初始化) – 當訪問一個靜態域時,只有真正宣告這個域的類才會被初始化
• 通過子類引用父類的靜態變數,不會導致子類初始化
– 通過陣列定義類引用,不會觸發此類的初始化
– 引用常量不會觸發此類的初始化(常量在編譯階段就存入呼叫類的常量池中了)

測試用例如下

package com.bigdataBC.jvm;

public class demo01 {
	static{
		System.out.println("靜態初始化Demo01");
	}
	
	
	public static void main(String[] args) throws Exception {
		System.out.println("Demo01的main方法!");
		System.out.println(System.getProperty("java.class.path"));
		
		//主動引用
//		new A();
//		System.out.println(A.width);
//		Class.forName("com.bjsxt.test.A");
		
		
		//被動引用
//		System.out.println(A.MAX);
//		A[] as = new A[10];
		System.out.println(B.width);
		
	}
}

class B  extends A {
	static {
		System.out.println("靜態初始化B");
	}
}

class A extends A_Father {
	public static int width=100;   //靜態變數,靜態域    field
	public static final  int MAX=100; 
	
	static {
		System.out.println("靜態初始化類A");
		width=300;
	}
	public A(){
		System.out.println("建立A類的物件");
	}
}

class A_Father extends Object {
	static {
		System.out.println("靜態初始化A_Father");
	}
}

2. 類載入器的作用

類載入器的作用
– 將class檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法
區中的執行時資料結構,在堆中生成一個代表這個類的java.lang.Class
物件,作為方法區類資料的訪問入口。
在這裡插入圖片描述
類快取
• 標準的Java SE類載入器可以按要求查詢類,但一旦某個類被載入到類載入
器中,它將維持載入(快取)一段時間。不過,JVM垃圾收集器可以回收
這些Class物件。

3. java.class.ClassLoader類

  • 作用:

    • java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,
      找到或者生成其對應的位元組程式碼,然後從這些位元組程式碼中定義出一個
      Java 類,即 java.lang.Class 類的一個例項。

    • 除此之外,ClassLoader還負責載入 Java 應用所需的資源,如影像文
      件和配置檔案等。

  • 相關方法

    • getParent() 返回該類載入器的父類載入器。
      loadClass(String name) 載入名稱為 name的類,返回的結果是 java.lang.Class 類的例項。
      findClass(String name) 查詢名稱為 name的類,返回的結果是 java.lang.Class類的例項。
      findLoadedClass(String name)查詢名稱為 name的已經被載入過的類,返回的結果是 java.lang.Class類的實
      例。
      defineClass(String name, byte[] b, int off, int len)把位元組陣列 b中的內容轉換成 Java 類,返回的結果是
      java.lang.Class 類的例項。這個方法被宣告為 final的。resolveClass(Class<?> c) 連結指定的 Java 類。
    • 對於以上給出的方法,表示類名稱的 name引數的值是類的二進位制名稱。需要注意的是內部類的表示,如
      com.example.Sample$1com.example.Sample$Inner等表示方式。

4. 類載入器的層次結構(樹狀結構)

  • 引導類載入器(bootstrap class loader)
    – 它用來載入 Java 的核心庫(JAVA_HOME/jre/lib/rt.jar,或sun.boot.class.path路徑下的
    內容),是用原生程式碼來實現的(這個是用C++寫的,後面介紹的三個載入器是用Java寫的),並不繼承自 java.lang.ClassLoader。
    在這裡插入圖片描述

    – 載入擴充套件類和應用程式類載入器。並指定他們的父類載入器。

  • 擴充套件類載入器(extensions class loader)
    – 用來載入 Java 的擴充套件庫(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路徑下的內容) 。
    Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java
    類。
    在這裡插入圖片描述

– 由sun.misc.Launcher$ExtClassLoader實現

  • 應用程式類載入器(application class loader)
    – 它根據 Java 應用的類路徑(classpath,
    java.class.path 路徑下的內容)來載入 Java 類。
    一般來說,Java 應用的類都是由它來完成載入的。
    – 由sun.misc.Launcher$AppClassLoader實現
  • 自定義類載入器
    – 開發人員可以通過繼承 java.lang.ClassLoader類的方式
    實現自己的類載入器,以滿足一些特殊的需求。
    在這裡插入圖片描述
    上圖表示的雖然看似是父子關係,其實不是繼承而是組合的關係。

在這裡插入圖片描述

5. 類載入器的代理模式

代理模式
– 交給其他載入器來載入指定的類
• 雙親委託機制
– 就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委
託給父類載入器,依次追溯,直到最高的爺爺輩的,如果父類載入器
可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入
任務時,才自己去載入。
– 雙親委託機制是為了保證 Java 核心庫的型別安全。
• 這種機制就保證不會出現使用者自己能定義java.lang.Object類的情況。
– 類載入器除了用於載入類,也是安全的最基本的屏障。
• 雙親委託機制是代理模式的一種
– 並不是所有的類載入器都採用雙親委託機制。
– tomcat伺服器類載入器也使用代理模式,所不同的是它是首先嚐試去載入某個類,如果找不到再代理給父類載入器。
這與一般類載入器的順序是相反的

6. java.class.ClassLoader類API

• 相關方法
– getParent() 返回該類載入器的父類載入器。
– loadClass(String name) 載入名稱為 name的類,返回的結果是 java.lang.Class類的例項。
• 此方法負責載入指定名字的類,首先會從已載入的類中去尋找,如果沒有找到;從parent ClassLoader[ExtClassLoader]中載入;如
果沒有載入到,則從Bootstrap ClassLoader中嘗試載入(findBootstrapClassOrNull方法), 如果還是載入失敗,則自己載入。如果還
不能載入,則丟擲異常ClassNotFoundException。
• 如果要改變類的載入順序可以覆蓋此方法;
– findClass(String name) 查詢名稱為 name的類,返回的結果是 java.lang.Class類的例項。
– findLoadedClass(String name) 查詢名稱為 name的已經被載入過的類,返回的結果是 java.lang.Class類的實
例。
– defineClass(String name, byte[] b, int off, int len) 把位元組陣列 b中的內容轉換成 Java 類,返回的結果是
java.lang.Class類的例項。這個方法被宣告為 final的。
– resolveClass(Class<?> c) 連結指定的 Java 類。

– 表示類名稱的 name引數的值是類的名稱。需要注意的是內部類的表示,如 com.example.Sample 1 和 c o m . e x a m p l e . S a m p l e 1和 com.example.Sample 1com.example.SampleInner等表示方式。

7. 自定義類載入器

• 檔案系統類載入器
• 自定義類載入器的流程:
– 1、首先檢查請求的型別是否已經被這個類裝載器裝載到名稱空間中了,如果已經裝載,直接返回;否則轉入步驟2
– 2、委派類載入請求給父類載入器(更準確的說應該是雙親類載入器,真個虛擬機器中各種類載入器最終會呈現樹狀結構),如果父類加
載器能夠完成,則返回父類載入器載入的Class例項;否則轉入步驟3
– 3、呼叫本類載入器的findClass(…)方法,試圖獲取對應的位元組碼,如果獲取的到,則呼叫defineClass(…)匯入型別到方法區;如
果獲取不到對應的位元組碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉拋異常,終止載入過程(注意:這裡的
異常種類不止一種)。
– 注意:被兩個類載入器載入的同一個類,JVM不認為是相同的類。

7.1 檔案類載入器

先準備一個編譯好的 class 檔案
在這裡插入圖片描述
定義一個檔案類載入器

package com.bigdataBC.loader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 自定義檔案系統類載入器
 *
 */
public class FileSystemClassLoader extends ClassLoader {
	
	//com.bigdataBC.loader.HelloWorld   --> d:/myjava/  com/bigdataBC/loader/HelloWorld.class      
	private String rootDir;
	
	public FileSystemClassLoader(String rootDir){
		this.rootDir = rootDir;
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		
		Class<?> c = findLoadedClass(name);
		
		//應該要先查詢有沒有載入過這個類。如果已經載入,則直接返回載入好的類。如果沒有,則載入新的類。
		if(c!=null){
			return c;
		}else{
			ClassLoader parent = this.getParent();
			try {
				c = parent.loadClass(name);	   //委派給父類載入
			} catch (Exception e) {
//				e.printStackTrace();
			}
			
			if(c!=null){
				return c;
			}else{
				byte[] classData = getClassData(name);
				if(classData==null){
					throw new ClassNotFoundException();
				}else{
					c = defineClass(name, classData, 0,classData.length);
				}
			}
			
		}
		
		return c;
	}
	
	private byte[] getClassData(String classname){   //com.bigdataBC.loader.HelloWorld   --> d:/myjava/  com/bigdataBC/loader/HelloWorld.class 
		String path = rootDir +"/"+ classname.replace('.', '/')+".class";
		
//		IOUtils,可以使用它將流中的資料轉成位元組陣列
		InputStream is = null;
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try{
			is  = new FileInputStream(path);
			
			byte[] buffer = new byte[1024];
			int temp=0;
			while((temp=is.read(buffer))!=-1){
				baos.write(buffer, 0, temp);
			}
			return baos.toByteArray();
		}catch(Exception e){
			e.printStackTrace();
			return null;
		}finally{
			try {
				if(is!=null){
					is.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				if(baos!=null){
					baos.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

測試類

package com.bigdataBC.loader;

/**
 * 測試自定義的FileSystemClassLoader
 *
 */
public class demo {
	public static void main(String[] args) throws Exception{
		FileSystemClassLoader loader = new FileSystemClassLoader("d:/myjava");
		FileSystemClassLoader loader2 = new FileSystemClassLoader("d:/myjava");
		
		Class<?> c = loader.loadClass("com.bigdataBC.loader.HelloWorld");
		Class<?> c2 = loader.loadClass("com.bigdataBC.loader.HelloWorld");
		Class<?> c3 = loader2.loadClass("com.bigdataBC.loader.HelloWorld");

		Class<?> c4 = loader2.loadClass("java.lang.String");
		Class<?> c5 = loader2.loadClass("com.bigdataBC.loader.demo");
		
		
		System.out.println(c.hashCode());
		System.out.println(c2.hashCode());
		System.out.println(c3.hashCode());	//同一個類,被不同的載入器載入,JVM認為也是不相同的類
		System.out.println(c4.hashCode());
		System.out.println(c3.getClassLoader());	//自定義的類載入器
		System.out.println(c4.getClassLoader());	//引導類載入器
		System.out.println(c5.getClassLoader());	//系統預設的類載入器
		
	}
}

在這裡插入圖片描述

7.2 網路類載入器

和上面的例子大同小異,無非就是把本地路徑換成URL路徑

package com.bjsxt.test;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

/**
 * 網路類載入器
 *
 */
public class NetClassLoader extends ClassLoader {
	
	//com.bigdataBC.loader.HelloWorld   --> www.xxx.cn/myjava/  com/bigdataBC/loader/HelloWorld.class    
	private String rootUrl;
	
	public NetClassLoader(String rootUrl){
		this.rootUrl = rootUrl;
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		
		Class<?> c = findLoadedClass(name);
		
		//應該要先查詢有沒有載入過這個類。如果已經載入,則直接返回載入好的類。如果沒有,則載入新的類。
		if(c!=null){
			return c;
		}else{
			ClassLoader parent = this.getParent();
			try {
				c = parent.loadClass(name);	   //委派給父類載入
			} catch (Exception e) {
//				e.printStackTrace();
			}
			
			if(c!=null){
				return c;
			}else{
				byte[] classData = getClassData(name);
				if(classData==null){
					throw new ClassNotFoundException();
				}else{
					c = defineClass(name, classData, 0,classData.length);
				}
			}
			
		}
		
		return c;
		
	}
	
	private byte[] getClassData(String classname){   //com.bigdataBC.loader.HelloWorld   --> www.xxx.cn/myjava/  com/bigdataBC/loader/HelloWorld.class 
		String path = rootUrl +"/"+ classname.replace('.', '/')+".class";
		
//		IOUtils,可以使用它將流中的資料轉成位元組陣列
		InputStream is = null;
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try{
			URL url = new URL(path);
			is  = url.openStream();
			
			byte[] buffer = new byte[1024];
			int temp=0;
			while((temp=is.read(buffer))!=-1){
				baos.write(buffer, 0, temp);
			}
			
			return baos.toByteArray();
		}catch(Exception e){
			e.printStackTrace();
			return null;
		}finally{
			try {
				if(is!=null){
					is.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				if(baos!=null){
					baos.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
	}
}

7.3 加密解密類載入器(取反操作,DES對稱加密解密)

一個簡單的加密工具類

package com.bjsxt.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 加密工具類
 *
 */
public class EncrptUtil {
	
	public static void main(String[] args) {
		encrpt("d:/myjava/com/bigdataBC/loader/HelloWorld.class", "d:/myjava/temp/HelloWorld.class");
	}
	
	public static void encrpt(String src, String dest){
		FileInputStream fis = null;
		FileOutputStream fos = null;
		
		try {
			fis = new FileInputStream(src);
			fos = new FileOutputStream(dest);
			
			int temp = -1;
			while((temp=fis.read())!=-1){
				fos.write(temp^0xff);  //取反操作
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				if(fis!=null){
					fis.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				if(fos!=null){
					fos.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

在這裡插入圖片描述
執行該工具類後在 temp 下面生成一個新的加密過的位元組碼檔案。
那麼如何解密呢?再來定義一個與之對應的解密工具類:

package com.bjsxt.test;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 載入檔案系統中加密後的class位元組碼的類載入器
 *
 */
public class DecrptClassLoader  extends ClassLoader {
	
	//com.bigdataBC.loader.HelloWorld   --> d:/myjava/  com/bigdataBC/loader/HelloWorld.class    
	private String rootDir;
	
	public DecrptClassLoader(String rootDir){
		this.rootDir = rootDir;
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		
		Class<?> c = findLoadedClass(name);
		
		//應該要先查詢有沒有載入過這個類。如果已經載入,則直接返回載入好的類。如果沒有,則載入新的類。
		if(c!=null){
			return c;
		}else{
			ClassLoader parent = this.getParent();
			try {
				c = parent.loadClass(name);	   //委派給父類載入
			} catch (Exception e) {
//				e.printStackTrace();
			}
			
			if(c!=null){
				return c;
			}else{
				byte[] classData = getClassData(name);
				if(classData==null){
					throw new ClassNotFoundException();
				}else{
					c = defineClass(name, classData, 0,classData.length);
				}
			}
			
		}
		
		return c;
		
	}
	
	private byte[] getClassData(String classname){   //com.bigdataBC.loader.HelloWorld   --> d:/myjava/  com/bigdataBC/loader/HelloWorld.class  
		String path = rootDir +"/"+ classname.replace('.', '/')+".class";
		
//		IOUtils,可以使用它將流中的資料轉成位元組陣列
		InputStream is = null;
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try{
			is  = new FileInputStream(path);
			
			
			
			int temp = -1;
			while((temp=is.read())!=-1){
				baos.write(temp^0xff);  //取反操作,相當於解密
			}
			
			return baos.toByteArray();
		}catch(Exception e){
			e.printStackTrace();
			return null;
		}finally{
			try {
				if(is!=null){
					is.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				if(baos!=null){
					baos.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

package com.bjsxt.test;

/**
 * 測試簡單加密解密(取反)操作
 *
 */
public class Demo04 {
	public static void main(String[] args) throws Exception {
		//測試取反操作
//		int a = 3; //0000011
//		System.out.println(Integer.toBinaryString(a^0xff));
		
		//加密後的class檔案,正常的類載入器無法載入,報classFormatError
//		FileSystemClassLoader loader = new FileSystemClassLoader("d:/myjava/temp");
//		Class<?> c = loader.loadClass("HelloWorld");
//		System.out.println(c);
		
		DecrptClassLoader loader = new DecrptClassLoader("d:/myjava/temp");
		Class<?> c = loader.loadClass("com.bigdataBC.loader.HelloWorld");
		System.out.println(c);
		
	}
}

在這裡插入圖片描述

8. 執行緒上下文類載入器

• 雙親委託機制以及預設類載入器的問題
– 一般情況下, 保證同一個類中所關聯的其他類都是由當前類的類載入器所載入的.。
比如,ClassA本身在Ext下找到,那麼他裡面new出來的一些類也就只能用Ext去查詢了(不會低一個級別),所以有
些明明App可以找到的,卻找不到了。
– JDBC API,他有實現的driven部分(mysql/sql server),我們的JDBC API都是由Boot或者Ext來載入的,但是
JDBC driver卻是由Ext或者App來載入,那麼就有可能找不到driver了。在Java領域中,其實只要分成這種Api+SPI(
Service Provide Interface,特定廠商提供)的,都會遇到此問題。
– 常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的介面由 Java 核心庫來提供,如 JAXP 的 SPI 介面定
義包含在 javax.xml.parsers 包中。SPI 的介面是 Java 核心庫的一部分,是由引導類載入器來載入的;SPI 實現的
Java 類一般是由系統類載入器來載入的。引導類載入器是無法找到 SPI 的實現類的,因為它只載入 Java 的核心庫

• 通常當你需要動態載入資源的時候 , 你至少有三個 ClassLoader 可以選擇 : – 1.系統類載入器或叫作應用類載入器 (system classloader or application classloader) – 2.當前類載入器
– 3.當前執行緒類載入器
• 當前執行緒類載入器是為了拋棄雙親委派載入鏈模式。
– 每個執行緒都有一個關聯的上下文類載入器。如果你使用new Thread()方式生成新的執行緒,新執行緒將繼承其父執行緒的上
下文類載入器。如果程式對執行緒上下文類載入器沒有任何改動的話,程式中所有的執行緒將都使用系統類載入器作為上
下文類載入器。
• Thread.currentThread().getContextClassLoader()

package com.bjsxt.test;


/**
 * 執行緒上下文類載入器的測試
 */
public class Demo05 {
	public static void main(String[] args) throws Exception {
		ClassLoader loader = Demo05.class.getClassLoader();
		System.out.println(loader);
		
		
		ClassLoader loader2 = Thread.currentThread().getContextClassLoader();
		System.out.println(loader2);
		
		Thread.currentThread().setContextClassLoader(new FileSystemClassLoader("d:/myjava/"));
		System.out.println(Thread.currentThread().getContextClassLoader());
		
		Class<Demo01> c = (Class<Demo01>) Thread.currentThread().getContextClassLoader().loadClass("com.bigdataBC.loader.HelloWorld");
		System.out.println(c);
		System.out.println(c.getClassLoader());
		
	}
}

在這裡插入圖片描述

9. TOMCAT伺服器的類載入機制

• 一切都是為了安全!
– TOMCAT不能使用系統預設的類載入器。
• 如果TOMCAT跑你的WEB專案使用系統的類載入器那是相當危險的,你可
以直接是無忌憚是作業系統的各個目錄了。
• 對於執行在 Java EE™容器中的 Web 應用來說,類載入器的實現方式與一
般的 Java 應用有所不同。
• 每個 Web 應用都有一個對應的類載入器例項。該類載入器也使用代理模
式(不同於前面說的雙親委託機制),所不同的是它是首先嚐試去載入某個
類,如果找不到再代理給父類載入器。這與一般類載入器的順序是相反的
。但也是為了保證安全,這樣核心庫就不在查詢範圍之內。
• 為了安全TOMCAT需要實現自己的類載入器。
• 我可以限制你只能把類寫在指定的地方,否則我不給你載入!
在這裡插入圖片描述
OSGI原理介紹
• OSGi™是 Java 上的動態模組系統。它為開發人員提供了面向服務和基於元件的運
行環境,並提供標準的方式用來管理軟體的生命週期。
• OSGi 已經被實現和部署在很多產品上,在開源社群也得到了廣泛的支援。Eclipse
就是基於 OSGi 技術來構建的。
• 原理:
– OSGi 中的每個模組(bundle)都包含 Java 包和類。模組可以宣告它所依賴的需要匯入
(import)的其它模組的 Java 包和類(通過 Import-Package),也可以宣告匯出(
export)自己的包和類,供其它模組使用(通過 Export-Package)。也就是說需要能
夠隱藏和共享一個模組中的某些 Java 包和類。這是通過 OSGi 特有的類載入器機制來
實現的。OSGi 中的每個模組都有對應的一個類載入器。它負責載入模組自己包含的
Java 包和類。當它需要載入 Java 核心庫的類時(以 java開頭的包和類),它會代理給
父類載入器(通常是啟動類載入器)來完成。當它需要載入所匯入的 Java 類時,它會
代理給匯出此 Java 類的模組來完成載入。模組也可以顯式的宣告某些 Java 包和類,必
須由父類載入器來載入。只需要設定系統屬性 org.osgi.framework.bootdelegation
的值即可

相關文章