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

Java 类加载器解析及常见类加载问题的更多相关文章

  1. Java中的类加载器以及Tomcat的类加载机制

    在加载阶段,虚拟机需要完成以下三件事情: 1.通过一个类的全限定名来获取其定义的二进制字节流. 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构. 3.在Java堆中生成一个代表这个类 ...

  2. java类加载器学习2——自定义类加载器和父类委托机制带来的问题

    一.自定义类加载器的一般步骤 Java的类加载器自从JDK1.2开始便引入了一条机制叫做父类委托机制.一个类需要被加载的时候,JVM先会调用他的父类加载器进行加载,父类调用父类的父类,一直到顶级类加载 ...

  3. JAVA类加载器概念与线程类加载器

    类加载器的功能:通过一个类的全限定名来获取描述此类的二进制字节流的过程 java的类加载器大致可以分为两类,一类是系统提供的,一类是由应用开发人员编写的.系统提供的类加载器有以下三种: 引导类加载器( ...

  4. JVM 核心机制(类加载器、自定义文件系统类加载器、网络自定义类加载器

  5. JVM核心机制(类加载器、三种类加载器、代理加载模式、双亲委派机制

  6. 深入理解Java类加载器(1):Java类加载原理解析

    1 基本信息 每个开发人员对Java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载.Java的类加载机制是技术体系中比较核心的 ...

  7. 深入理解Java类加载器(一):Java类加载原理解析

    摘要: 每个开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这个异常背后涉及到的是Java技术体系中的类加载机制.本文简述了JVM三种预定义类加载器,即 ...

  8. java笔记--理解java类加载器以及ClassLoader类

    类加载器概述: java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制 ...

  9. Java内存管理-掌握类加载器的核心源码和设计模式(六)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有 ...

随机推荐

  1. 我的webpack学习笔记(二)

    前言 上一篇文章我们讲了多页面js的打包,本篇文章我们继续scss的打包. 多页面css单独打包 首先,我们css编写采用的是sass,所以我们先来安装sass-loader以及可以用到的依赖 $ n ...

  2. 微信小程序实战(一)之仿美丽说

    被美丽说少女粉吸引,就想着自己也写一个来练练手,正好最近在学习微信小程序.接下来让我们分享一下我的学习历程吧! 选题 其实纠结了好久该仿什么,看到别人都写的差不多了,自己却还没有动手,很着急,那两天一 ...

  3. 【JavaScript】DOM之表单操作

    DOM 表单操作 1.获取表单 获取表单元素 以Document对象中forms属性来获取当前HTML页面所有表单集合以Document对象中表单的name属性值来获取表单元元素 <body&g ...

  4. 提高开发效率之VS Code基础配置篇

    背景 之前一直是只用WebStorm作为IDE来编写代码,但是由于: 手中的这台Mac接了两个显示器以后,使用WebStorm会有卡顿. WebStorm需要付费(虽然可以通过某方法和谐). 所以需要 ...

  5. 利用wps创建有目录的PDF/word

    为什么要创建: 在阅读一些行业规范或者很长的文件,像是项目管理方案时,非常麻烦,定位需要重新返回目录去.--->所以我想能不能创建一个带目录的PDF,可以点击直接跳转,那就方便多了. 如何创建: ...

  6. es6的解构函数

    话说,解构无处不在啊,鄙人自从用了vue写项目以来,总是遇到各路大神莫名其妙的写法,然并未出任何错,查之,然解构也,呜呼哀哉,进而习之. 解构(Destructuring):是将一个数据结构分解为更小 ...

  7. Node的require和module.exports

    node编程中最重要的思想之一就是模块,在 Node.js 模块系统中,每个文件都被视为独立的模块.这是这个思想,让javascript的大规模工程成为可能.模块化编程在前端大肆盛行,在node中导出 ...

  8. 硬核数据结构,让你从B树理解到B+树

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是周五分布式系统的第八篇文章,核心内容是B+树的原理. 今天的文章是上周B树的延伸,所以新关注的或者是有所遗忘的同学建议先从下方链接回顾 ...

  9. C 和 C++语言中的内存拷贝函数memcpy()

    memcpy指的是C和C++使用的内存拷贝函数 函数原型为void *memcpy(void *destin, void *source, unsigned n): 函数的功能是从源内存地址的起始位置 ...

  10. BigInteger实现除法取余

    BigInteger实现除法取余 BigInteger是什么? Java中,整形的最大范围是64位的long型整数.但是如果我们使用的整数超过了64位呢?这时候就用到了BigInteger.BigIn ...