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 类加载器剖析及常见类加载问题

请注意,类加载器条理结构不是继续条理结构,而是委托条理结构。大多数类加载器在搜索自己的类路径之前将查找类和资源委托给其父类。若是父类加载器找不到类或资源,则类加载器只能实验在内陆找到它们。现实上,类加载器只卖力加载父级不可用的类;条理结构中较高的类加载器加载的类不能引用条理结构中较低的可用类。类加载器委托行为的念头是制止多次加载同一个类。

在 Java EE 中,查找的顺序通常是相反的:类加载器可能在转到父类之前实验在内陆查找类。

Java EE 委托模子

下面是应用程序容器的类加载器条理结构的典型视图:容器自己有一个类加载器,每个 EAR 模块都有自己的类加载器,每个 WAR 都有自己的类加载器。 Java Servlet 规范建议 web 模块的类加载器在委托给其父类之前先在内陆类加载器中查找——父类加载器只要求提供模块中找不到的资源和类。

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

在某些应用程序容器中,遵照此建议,但在其他应用程序容器中,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 属性值。

面试:给我说说你平时是如何优化MySQL的?

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

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 类加载器剖析及常见类加载问题

但它们为什么不相容呢?原来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 类加载器备忘单

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

原创文章,作者:admin,如若转载,请注明出处:https://www.2lxm.com/archives/4357.html