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. ajax+lazyload时lazyload失效问题及解决

    最近写公司的项目的时候遇到一个关于图片加载的问题,所做的页面是一个商城的商品列表页,其中需要显示商品图片,名称等信息,因为商品列表可能会很长,所以其中图片需要滑到可以显示的区域再进行加载. 首先我的图 ...

  2. 原型模式故事链(3)--JS的数据类型、以及区别、区分、转化

    上一章--原型链讲解:传送门:https://segmentfault.com/a/11... 在上一章讲解原型链时提到了:所有的引用类型都有一个_proto_属性,称之为隐式原型.那么引用类型是什么 ...

  3. 大厂常问iOS面试题--性能优化篇

    1.造成tableView卡顿的原因有哪些? 1.最常用的就是cell的重用, 注册重用标识符 如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell 如果有很多数据的时候 ...

  4. Java基础--方法的定义

    1.为什么要有方法? 方法(又叫函数)就是一段特定功能的代码块.方法提高程序的复用性和可读性. 比如,有了方法,我们可以把要重复使用的一段代码提炼出来,然后在每个需要执行这段代码的地方去调用即可. 2 ...

  5. 爬取疫情数据,以django+pyecharts实现数据可视化web网页

    在家呆着也是呆着,不如做点什么消磨时间呗~ 试试用django+pyecharts实现疫情数据可视化web页面 这里要爬疫情数据 来自丁香园.搜狗及百度的疫情实时动态展示页 先看看劳动成果: 导航栏: ...

  6. Rust入坑指南:齐头并进(上)

    我们知道,如今CPU的计算能力已经非常强大,其速度比内存要高出许多个数量级.为了充分利用CPU资源,多数编程语言都提供了并发编程的能力,Rust也不例外. 聊到并发,就离不开多进程和多线程这两个概念. ...

  7. 关于beforeRouteEnter中的next()

    beforeRouteEnter(to,from, next){ console.log(this)   //undefined next(vm => { console.log(vm)}) } ...

  8. 《JavaScript 模式》读书笔记(2)— 基本技巧2

    前一篇,简单介绍了一些js代码的基本技巧.那么这篇文章,我们继续后续的内容. 一.for循环 for循环经常用在遍历数组或者类数组对象,如引数(arguments)和HTML容器(HTMLCollti ...

  9. nodejs中的并发编程

    从sleep的实现说起 在nodejs中,如果要实现sleep的功能主要是通过"setTimeout + promise"实现,也可以通过"循环空转"来解决.前 ...

  10. kafka使用指南

    快速开始 本教程假设您刚刚开始,并且没有现有的Kafka或ZooKeeper数据.由于Kafka控制台脚本对于基于Unix和Windows的平台是不同的,因此在Windows平台上使用bin\wind ...