在许多开发人员中,类加载器是Java语言的底层,并且经常被忽略。 在ZeroTurnaround上 ,我们的开发人员必须生活,呼吸,饮食,喝酒,并且几乎与类加载器保持亲密关系,才能生产JRebel技术,该技术在类加载器级别进行交互以提供实时运行时类重装,从而避免了冗长的重建/重新包装/重新部署周期。
以下是我们从类加载器中学到的一些知识,其中包括一些调试技巧,这些技巧将有望为您节省时间和将来的总服务台。
一个类加载器只是一个普通的java对象
是的,这并不聪明,除了JVM中的系统类加载器之外,类加载器只是一个Java对象! 这是一个抽象类ClassLoader,可以由您创建的类实现。 这是API:
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,它仅使用String类名,然后返回实际的Class对象。 如果您以前使用过类加载器,则此方法可能是最熟悉的方法,因为它是日常编码中使用最多的方法。 defineClass是JVM中的最终方法,该方法从网络上的文件或位置获取字节数组,并产生相同的结果(即Class对象)。
类加载器还可以从类路径中找到资源。 它的工作方式与loadClass方法类似。 有两种方法,getResource和getResources,它们返回一个URL或URL的枚举,这些URL或URL的枚举指向资源,该资源表示传递给方法的名称。
每个类加载器都有一个父级。 getParent返回与Java继承无关的classloader父类,而是一个链表样式的连接。 稍后我们将对此进行更深入的研究。
类加载器是惰性的,因此仅在运行时请求类时才加载类。 类是由调用该类的资源加载的,因此,在运行时,一个类可以由多个类加载器加载,具体取决于从何处引用它们,以及哪个类加载器加载了引用了这些类的类…哎呀,我cross地了! 让我们看一些代码。
public class A {public void doSmth() {B b = new B();b.doSmthElse();}}
在这里,我们有一个类A在其方法范围内调用类B的构造函数。 在幕后这是正在发生的事情
A.class.getClassLoader().loadClass(“B”);
最初加载类A的类加载器被调用以加载类B。
类加载器是分层的,但是像孩子一样,他们并不总是问父母
每个类加载器都有一个父类加载器。 当一个类加载器被要求提供一个类时,它通常会直接转到父类加载器,首先调用loadClass,而后者又会询问它的父类,依此类推。 如果要求具有相同父级的两个类加载器加载同一类,则父级将只执行一次。 当两个类加载器分别加载同一个类时,这将非常麻烦,因为这可能会导致问题,我们将在后面讨论。
当设计JEE规范时,Web类加载器被设计为以相反的方式工作-很棒。 让我们看一下下图作为示例。
模块WAR1有自己的类加载器,并且更喜欢自行加载类,而不是委托给其父级(由App1.ear定义的类加载器)。 这意味着不同的WAR模块(例如WAR1和WAR2)无法看到彼此的类。 App1.ear模块具有自己的类加载器,并且是WAR1和WAR2类加载器的父级。 当WAR1和WAR2类加载器需要在层次结构中委派请求时,即WAR类加载器范围之外需要一个类时,它们将使用App1.ear类加载器。 实际上,WAR类会覆盖同时存在的EAR类。 最后,EAR类加载器的父级是容器类加载器。 EAR类加载器会将请求委派给容器类加载器,但它的执行方式与WAR类加载器不同,因为EAR类加载器实际上更喜欢委托而不是本地类。 如您所见,这变得非常繁琐,并且与普通的JSE类加载行为不同。
平面类路径
我们讨论了系统类加载器如何通过类路径查找已请求的类。 该类路径可能包含目录或JAR文件,查找它们的顺序实际上取决于您使用的JVM。 您在类路径上可能需要该类的多个副本或版本,但是您将始终在类路径上找到该类的第一个实例。 本质上,这只是资源列表,这就是为什么将其称为扁平资源。 结果,在查找资源时,遍历类路径列表通常会比较慢。
当使用相同类路径的应用程序想要使用类的不同版本时,可能会发生问题,让我们以Hibernate为例。 当类路径上存在两个版本的Hibernate JAR时,一个版本不能比一个应用程序的版本路径在另一个应用程序的类路径上更高,这意味着两个版本都必须使用相同的版本。 解决此问题的一种方法是使用所有必需的库使应用程序(WAR)膨胀,以便它们使用其本地资源,但这会导致难以维护的大型应用程序。 欢迎来到JAR地狱! OSGi在此提供了一种解决方案,因为它允许对JAR文件或捆绑软件进行版本控制,从而形成一种机制,允许连接到特定版本的JAR文件,从而避免了平坦的类路径问题。
如何调试类加载错误?
NoClassDefFoundError / ClassNotFoundException / ClassNoDefFoundException?
因此,您遇到了上述错误/异常。 好吧,这个班级真的存在吗? 不要在IDE中寻找麻烦,因为在那儿编译类是必须的,因为它必须在那里,否则您将获得编译时异常。 这是一个运行时异常,因此在运行时我们要查找它说我们缺少的类……但是您从哪里开始呢? 考虑下面的代码…
Arrays.toString((((URLClassLoader) Test.class.getClassLoader()).getURLs()));
此代码返回Test正在使用的类加载器的类路径上所有jar和目录的数组列表。 现在,我们可以看到神秘类应该存在的JAR或位置实际上在类路径上。 如果不存在,请添加! 如果确实存在,请检查JAR /目录,以确保您的类确实存在于该位置,并在缺少该类时添加它。 这是导致此错误情况的两个典型问题。
NoSuchMethodError / NoSuchFieldError / AbstractMethodError / IllegalAccessError吗?
现在变得越来越有趣了! 这些都是IncompatibleClassChangeError的所有子类。 我们知道类加载器已经找到了想要的类(按名称),但是显然它没有找到正确的版本。
在这里,我们有一个称为Test的类,它正在调用另一个类Util,但是BANG –我们遇到了异常! 让我们看一下要调试的下一个代码片段:
Test.class.getClassLoader().getResource(Util.class.getName().replace('.', '/') + ".class");
我们在类Test的类加载器上调用getResource。 这将向我们返回Util资源的URL。 请注意,我们已替换了“。” 带有“ /”,并在字符串末尾添加“ .class”。 这会将我们正在寻找的类的包和类名(从类加载器的角度来看)更改为文件系统上的目录结构和文件名-简洁。 这将向我们显示我们已加载的确切类,并且可以确保它是正确的版本。 我们可以在命令提示符下在类上使用javap -private来查看字节码并检查实际存在的方法和字段。 您可以轻松地查看该类的结构,并验证是您还是疯了的Java运行时! 相信我,在一个或另一个阶段,您都会同时问这两个问题,几乎每次都是您!
LinkageError / ClassCastException / IllegalAccessError
如果两个不同的类加载器加载同一个类,并且它们尝试进行交互,则可能会发生这种情况。 是的,现在有点毛了。 这可能会导致问题,因为我们不知道它们是否将从同一位置加载类。 怎么会这样 让我们看下面的代码,它们仍然在Test类中:
Factory.instance().sayHello();
该代码看起来非常干净和安全,尚不清楚如何从此行出现错误。 我们正在调用静态工厂方法来获取Test类的实例,并在其上调用方法。 让我们看一下该支持图像,以显示引发异常的原因。
在这里,我们可以看到一个Web类加载器(加载了Test类)将优先使用本地类,因此,当它引用一个类时,将尽可能由Web类加载器加载。 到目前为止还算简单。 Test类使用Factory类来获取Util类的实例,这在Java中是很典型的做法,但是Factory类在WAR中并不存在,因为它是一个外部库。 这是没有问题的,因为Web类加载器可以委托给共享类加载器,后者可以看到Factory类。 请注意,共享类加载器现在正在加载它自己的Util类版本,因为当Factory实例化该类时,它使用了共享类加载器(如前面的第一个示例所示)。 Factory类将Util对象(由共享类加载器创建)返回给WAR,WAR然后尝试使用该类,并将该类有效地强制转换为同一类的潜在不同版本(Web类加载器可见的Util类) )。 繁荣!
我们可以在两个地方(Factory.instance()方法和Test类)中运行与以前相同的代码,以查看每个Util类从何处加载。
Test.class.getClassLoader().getResource(Util.class.getName().replace('.', '/') + ".class"));
希望这可以使您对类加载的世界有所了解,而不是不了解类加载器,现在可以带着恐惧和不确定性来欣赏它! 感谢您的阅读并将其制作到最后。 我们都希望您从ZeroTurnaround祝您圣诞快乐,新年快乐! 编码愉快!
参考: 在JVM的底层– Java出现日历博客中来自JCG合作伙伴 Simon Maple的类加载器 。
翻译自: https://www.javacodegeeks.com/2012/12/under-the-jvm-hood-classloaders.html