百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Java 类加载器解析及常见类加载问题

toyiye 2024-09-03 01:08 4 浏览 0 评论

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()

相关推荐

# Python 3 # Python 3字典Dictionary(1)

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中,格式如...

Python第八课:数据类型中的字典及其函数与方法

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值...

Python中字典详解(python 中字典)

字典是Python中使用键进行索引的重要数据结构。它们是无序的项序列(键值对),这意味着顺序不被保留。键是不可变的。与列表一样,字典的值可以保存异构数据,即整数、浮点、字符串、NaN、布尔值、列表、数...

Python3.9又更新了:dict内置新功能,正式版十月见面

机器之心报道参与:一鸣、JaminPython3.8的热乎劲还没过去,Python就又双叒叕要更新了。近日,3.9版本的第四个alpha版已经开源。从文档中,我们可以看到官方透露的对dic...

Python3 基本数据类型详解(python三种基本数据类型)

文章来源:加米谷大数据Python中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。在Python中,变量就是变量,它没有类型,我们所说的"类型"是变...

一文掌握Python的字典(python字典用法大全)

字典是Python中最强大、最灵活的内置数据结构之一。它们允许存储键值对,从而实现高效的数据检索、操作和组织。本文深入探讨了字典,涵盖了它们的创建、操作和高级用法,以帮助中级Python开发...

超级完整|Python字典详解(python字典的方法或操作)

一、字典概述01字典的格式Python字典是一种可变容器模型,且可存储任意类型对象,如字符串、数字、元组等其他容器模型。字典的每个键值key=>value对用冒号:分割,每个对之间用逗号,...

Python3.9版本新特性:字典合并操作的详细解读

处于测试阶段的Python3.9版本中有一个新特性:我们在使用Python字典时,将能够编写出更可读、更紧凑的代码啦!Python版本你现在使用哪种版本的Python?3.7分?3.5分?还是2.7...

python 自学,字典3(一些例子)(python字典有哪些基本操作)

例子11;如何批量复制字典里的内容2;如何批量修改字典的内容3;如何批量修改字典里某些指定的内容...

Python3.9中的字典合并和更新,几乎影响了所有Python程序员

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

Python3大字典:《Python3自学速查手册.pdf》限时下载中

最近有人会想了,2022了,想学Python晚不晚,学习python有前途吗?IT行业行业薪资高,发展前景好,是很多求职群里严重的香饽饽,而要进入这个高薪行业,也不是那么轻而易举的,拿信工专业的大学生...

python学习——字典(python字典基本操作)

字典Python的字典数据类型是基于hash散列算法实现的,采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。但它是无序的,包含的元素个数不限,值...

324页清华教授撰写【Python 3 菜鸟查询手册】火了,小白入门字典

如何入门学习python...

Python3.9中的字典合并和更新,了解一下

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

python3基础之字典(python中字典的基本操作)

字典和列表一样,也是python内置的一种数据结构。字典的结构如下图:列表用中括号[]把元素包起来,而字典是用大括号{}把元素包起来,只不过字典的每一个元素都包含键和值两部分。键和值是一一对应的...

取消回复欢迎 发表评论:

请填写验证码