Java 類加載器解析及常見類加載問題

Java 類加載器解析及常見類加載問題

java.lang.ClassLoader
每個類加載器本身也是個對象——一個繼承 java.lang.ClassLoader 的實例。每個類被其中一個實例加載。我們下面來看看 java.lang.ClassLoader 中的 API, 不太相關的部分已忽略。

package java.lang;

public abstract class ClassLoader {

public Class loadClass(String name);
protected Class defineClass(byte[] b);

public URL getResource(String name);
public Enumeration getResources(String name);

public ClassLoader getParent()
}
loadClass: 目前 java.lang.ClassLoader 中最重要的方法是 loadClass 方法,它獲取要加載的類的全限定名返回 Class 對象。

defineClass: defineClass 方法用於具體化 JVM 的類。byte 數組參數是加載自磁盤或其他位置的類字節碼。

getResource 和 getResources: 返回資源路徑。loadClass 大致相當於 defineClass(getResource(name).getBytes())。

getParent: 返回父加載器。

Java 的懶惰特性影響了類加載器的工作方式——所有事情都應該在最後一刻完成。類只有在以某種方式被引用時纔會被加載-通過調用構造函數、靜態方法或字段。看個例子:

類 A 實例化類 B:

public class A {
public void doSomething() {

B b = new B();
 b.doSomethingElse();

}
}
語句 B b = new B() 在語義上等同於 B b = A.class. getClassLoader().loadClass(“B”).newInstance()。如我們所見,Java 中的每個對象都與其類 (A.class) 相關聯,並且每個類都與用於加載類的類加載器 (A.class.getClassLoader()) 相關聯。

當我們實例化類加載器時,我們可以將父類加載器指定爲構造函數參數。如果未顯式指定父類加載器,則會將虛擬機的系統類加載器指定爲默認父類。

類加載器層次結構
每當啓動新的 JVM 時,引導類加載器(bootstrap classloader)負責首先將關鍵 Java 類(來自 Java.lang 包)和其他運行時類加載到內存中。引導類加載器是所有其他類加載器的父類。因此,它是唯一沒有父類的。

接下來是擴展類加載器(extension classloader)。引導類加載器(bootstrap classloader)作爲父類,負責從 java.ext.dirs 路徑中保存的所有 .jar 文件加載類。

從開發人員的角度來看,第三個也是最重要的類加載器是系統類路徑類加載器(system classpath classloader),它是擴展類加載器(extension classloader)的直接子類。它從由 CLASSPATH 環境變量 java.class.pat h系統屬性或 -classpath 命令行選項指定的目錄和 jar 文件加載類。

請注意,類加載器層次結構不是繼承層次結構,而是委託層次結構。大多數類加載器在搜索自己的類路徑之前將查找類和資源委託給其父類。如果父類加載器找不到類或資源,則類加載器只能嘗試在本地找到它們。實際上,類加載器只負責加載父級不可用的類;層次結構中較高的類加載器加載的類不能引用層次結構中較低的可用類。類加載器委託行爲的動機是避免多次加載同一個類。

在 Java EE 中,查找的順序通常是相反的:類加載器可能在轉到父類之前嘗試在本地查找類。

Java EE 委託模型
下面是應用程序容器的類加載器層次結構的典型視圖:容器本身有一個類加載器,每個 EAR 模塊都有自己的類加載器,每個 WAR 都有自己的類加載器。 Java Servlet 規範建議 web 模塊的類加載器在委託給其父類之前先在本地類加載器中查找——父類加載器只要求提供模塊中找不到的資源和類。

在某些應用程序容器中,遵循此建議,但在其他應用程序容器中,web 模塊的類加載器配置爲遵循與其他類加載器相同的委託模型,因此建議參考您使用的應用程序容器的文檔。

顛倒本地查找和委託查找之間的順序的原因是,應用程序容器附帶了許多具有自己的發佈週期的庫,這些庫可能不適用於應用程序開發人員。典型的例子是 log4j 庫——它的一個版本通常隨容器一起提供,不同的版本與應用程序捆綁在一起。

現在,讓我們來看看我們可能遇到的幾個常見的類加載問題,並提供可能的解決方案。

常見類加載問題
Java EE 委託模型會導致類加載的一些有趣的問題。NoClassDefFoundError、LinkageError、ClassNotFoundException、NoSuchMethodError、ClassCasteException等是開發 Java EE 應用程序時遇到的非常常見的異常。我們可以對這些問題的根本原因做出各種假設,但重要的是要驗證它們。

NoClassDefFoundError
NoClassDefFoundError 是開發 Java EE Java 應用程序時最常見的問題之一。

根本原因分析和解決過程的複雜性主要取決於 Java EE 中間件環境的大小;特別是考慮到各種 Java EE 應用程序中存在大量的類加載器。

正如 Javadoc 條目所說,如果 Java 虛擬機或類加載器實例試圖在類的定義中加載,而找不到類的定義,則拋出 NoClassDefFoundError。這意味着,在編譯當前執行的類時,搜索到的類定義存在,但在運行時找不到該定義。

這就是爲什麼你不能總是依賴你的 IDE 告訴你一切正常,代碼編譯應該正常工作。相反,這是一個運行時問題,IDE 在這裏無法提供幫助。

讓我們看看下面的例子:

public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,

                                HttpServletResponse response)
                                throws ServletException, IOException {
   PrintWriter out = response.getWriter();
   out.print(new Util().sayHello());

}
servlet HelloServlet 實例化了 Util 類的一個實例,該實例提供了要打印的消息。遺憾的是,當請求執行時,我們可能會看到以下內容:

java.lang.NoClassdefFoundError: Util
HelloServlet:doGet(HelloServlet.java:17)
javax.servlet.http.HttpServlet.service(HttpServlet.java:617)
javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
我們如何解決這個問題?好吧,您可能要做的最明顯的操作是檢查丟失的 Util 類是否已實際包含在包中。

我們在這裏可以使用的技巧之一是讓容器類加載器承認它從何處加載資源。爲此,我們可以嘗試將 HelloServlet 的類加載器轉換爲 URLClassLoader 並請求其類路徑。

public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,

                                HttpServletResponse response)
                                throws ServletException, IOException {
   PrintWriter out = response.getWriter();
   out.print(Arrays.toString(
       ((URLClassLoader)HelloServlet.class.getClassLoader()).getURLs()));

}
結果很可能是這樣:

file:/Users/myuser/eclipse/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/demo/WEB-INF/classes,
file:/Users/myuser/eclipse/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/demo/WEB-INF/lib/demo-lib.jar
資源的路徑(file:/Users/myuser/eclipse/workspace/.metadata/)實際上顯示容器是從 Eclipse 啓動的,這是 IDE 解壓歸檔文件來進行部署的地方。現在我們可以檢查丟失的 Util 是否真的包含在 demo-lib.jar 中,或者它是否存在於擴展存檔的 WEB-INF/classes 目錄中。

因此,對於我們的特定示例,可能是這樣的情況:Util 類應該打包到 demo-lib.jar 中,但是我們沒有重新啓動構建過程,並且該類沒有包含在以前存在的包中,因此出現了錯誤。

URLClassLoader 技巧可能不適用於所有應用服務器。另一種方法是使用jconsole 實用程序附加到容器JVM進程,以檢查類路徑。例如,屏幕截圖(如下)演示了連接到 JBoss application server 進程的 jconsole 窗口,我們可以從運行時屬性中看到 ClassPath 屬性值。

NoSuchMethodError
在另一個具有相同示例的場景中,我們可能會遇到以下異常:

java.lang.NoSuchMethodError: Util.sayHello()Ljava/lang/String;
HelloServlet:doGet(HelloServlet.java:17)
javax.servlet.http.HttpServlet.service(HttpServlet.java:617)
javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
NoSuchMethodError 代表另一個問題。在本例中,我們所引用的類存在,但加載的類版本不正確,因此找不到所需的方法。
要解決這個問題,我們首先必須瞭解類是從何處加載的。最簡單的方法是向 JVM 添加 '-verbose:class' 命令行參數,但是如果您可以快速更改代碼,那麼您可以使用 getResource 搜索與 loadClass 相同的類路徑。

public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,

                                HttpServletResponse response)
                                throws ServletException, IOException {
   PrintWriter out = response.getWriter();
out.print(HelloServlet.class.getClassLoader().getResource(
       Util.class.getName.replace(‘.’, ‘/’) + “.class”));  

}

假設,上述示例的請求執行結果如下.

file:/Users/myuser/eclipse/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/demo/WEB-INF/lib/demo-lib.jar!/Util.class
現在我們需要驗證關於類的錯誤版本的假設。我們可以使用javap實用程序來反編譯類,然後我們可以看到所需的方法是否實際存在。

$ javap -private Util
Compiled from “Util.java”
public class Util extends java.lang.Object {
public Util();
}
如您所見,Util 類的反編譯版本中沒有sayHello方法。可能,我們在 demo-lib.jar 中打包了 Util 類的初始版本,但是在添加了新的 sayHello 方法之後,我們沒有重新構建這個包。

在處理 Java EE 應用程序時,錯誤類問題 NoClassDefFoundError 和 NoSuchMethodError 的變體是非常典型的,這是 Java 開發人員理解這些錯誤的本質以有效解決問題所必需的技能。

這些問題有很多變體:AbstractMethodError、ClassCastException、IllegalAccessError——基本上,當我們認爲應用程序使用類的一個版本,但實際上它使用了其他版本,或者類的加載方式與需要的不同時,這些問題都會遇到。

ClassCastException
這裏我們只演示 ClassCastException 例子。我們將以使用工廠修改初始示例,以便提供提供問候消息的類的實現。這看起來很做作,但這是很常見的模式。

public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,

                                HttpServletResponse response) 
                                throws ServletException, IOException {
   PrintWriter out = response.getWriter();
out.print(((Util)Factory.getUtil()).sayHello());

}

class Factory {

 public static Object getUtil() {
      return new Util();
 }

}

請求的可能結果是:

java.lang.ClassCastException: Util cannot be cast to Util

HelloServlet:doGet(HelloServlet.java:18)
javax.servlet.http.HttpServlet.service(HttpServlet.java:617)
javax.servlet.http.HttpServlet.service(HttpServlet.java:717)

這意味着 HelloServlet 和 Factory 類在不同的上下文中操作。我們必須弄清楚這些類是如何加載的。讓我們使用 -verbose:class 並找出如何加載與HelloServlet 和 Factory 類相關的 Util 類。

[Loaded Util from file:/Users/ekabanov/Applications/ apache-tomcat-6.0.20/lib/cl-shared-jar.jar]
[Loaded Util from file:/Users/ekabanov/Documents/workspace-javazone/.metadata/.plugins/org.eclipse.wst. server.core/tmp0/wtpwebapps/cl-demo/WEB-INF/lib/cl-demo- jar.jar]
因此,Util類由不同的類加載器從兩個不同的位置加載。一個在web應用程序類加載器中,另一個在應用程序容器類加載器中。它們是不兼容的,不能相互轉換。

但它們爲什麼不相容呢?原來Java中的每個類都是由其完全限定名唯一標識的。但在1997年發表的一篇論文揭露了由此引起的一個廣泛的安全問題,即沙盒應用程序(例如: applet)可以定義任何類,包括 java.lang.String,並在沙盒外注入自己的代碼。

解決方案是通過完全限定名和類加載器的組合來標識類!這意味着從類加載器 A 加載的 Util 類和從類加載器 B 加載的 Util 類在 JVM 中是不同的類,不能將一個類轉換爲另一個類!

這個問題的根源是 web 類加載器的反向行爲。如果 web 類加載器的行爲與其他類加載器相同,那麼 Util 類將從應用程序容器類加載器加載一次,並且不會拋出類 CastException。

LinkageError
讓我們從前面的示例中稍微修改一下 Factory 類,這樣 getUtil 方法現在返回的是 Util 類型而不是 Object:

class Factory {

 public static Util getUtil() {
      return new Util();
 }

}
現在,執行的結果是 LinkageError:

ClassCastException: java.lang.LinkageError: loader constraint violation: when resolving method Factory.getUtil()LUtil;
<…> HelloServlet:doGet(HelloServlet.java:18)
javax.servlet.http.HttpServlet.service(HttpServlet.java:617) javax.servlet.http.HttpServlet.service(HttpServlet.java:717)

根本問題與 ClassCastException 相同——唯一的區別是我們不強制轉換對象,而是加載程序約束導致Linkage錯誤。

在處理類加載器時,一個非常重要的原則是認識到類加載器的行爲常常會破壞您的直觀理解,因此驗證您的假設非常重要。例如,在 LinkageError 的情況下,查看代碼或構建過程將阻礙而不是幫助您。關鍵是查看類的確切加載位置,它們是如何到達那裏的,以及如何防止將來發生這種情況。

多個類加載器中存在相同類的一個常見原因是,同一個庫的不同版本捆綁在不同的位置,例如應用服務器和 web 應用程序。這通常發生在像 log4j 或 hibernate 這樣的實際標準庫中。在這種情況下,解決方案要麼是將庫與 web 應用程序分開,要麼是非常小心地避免使用父類加載器中的類。

IllegalAccessError
其實,不僅類由其全限定名和類加載器標識,而且該規則也適用於包。爲了演示這一點,我們將 Factory.getUtil 方法的訪問修飾符更改爲默認值:

class Factory {

 static Object getUtil() {
      return new Util();
 }

}

假設 HelloServlet 和 Factory 都位於同一個(默認)包中,因此 getUtil 在 HelloServlet 類中可見。不幸的是,如果我們試圖在運行時訪問它,我們將看到 IllegalAccessError 異常。

java.lang.IllegalAccessError: tried to access method Factory.getUtil()Ljava/lang/Object;
HelloServlet:doGet(HelloServlet.java:18)
javax.servlet.http.HttpServlet.service(HttpServlet.java:617)
javax.servlet.http.HttpServlet.service(HttpServlet.java:717)

儘管訪問修飾符對於應用程序的編譯是正確的,但是在運行時,這些類是從不同的類加載器加載的,應用程序無法運行。這是由於與類一樣,包也由它們的完全限定名和類加載器來標識,出於同樣的安全原因。

ClassCastException、LinkageError 和 IllegalAccessError 根據實現有點不同,但根本原因是相同的類被不同的類加載器加載。

Java 類加載器備忘單

No class found
Variants

ClassNotFoundException
NoClassDefFoundError
Helpful

IDE class lookup (Ctrl+Shift+T in Eclipse)
find *.jar -exec jar -tf '{}'; | grep MyClass
URLClassLoader.getUrls() Container specific logs
Wrong class found
Variants

IncompatibleClassChangeError AbstractMethodError NoSuch(Method|Field)Error
ClassCastException, IllegalAccessError
Helpful

-verbose:class
ClassLoader.getResource() javap -private MyClass
More than one class found
LinkageError (class loading constraints violated)
ClassCastException, IllegalAccessError
Helpful

-verbose:class
ClassLoader.getResource()
參考鏈接:

https://www.jrebel.com/blog/how-to-use-java-classloaders
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html

原文地址https://www.cnblogs.com/flythinking/p/12643249.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章