类加载器负责将类加载到Java虚拟机(JVM)中。 简单的应用程序可以使用Java平台的内置类加载工具来加载其类。 更复杂的应用程序倾向于定义自己的自定义类加载器。 但是,无论使用哪种类型的类加载器,在类加载过程中都可能会发生许多问题。 如果要避免此类问题,则需要了解类加载的基本机制。 当确实发生问题时,对可用诊断功能和调试技术的了解应有助于您解决问题。
在本系列文章中,我们将深入研究类加载问题,并使用综合示例进行说明。 本介绍性文章的第一部分描述了类加载的基本知识。 第二部分介绍了一些JVM调试功能。 本系列的下三篇文章将重点介绍如何解决类加载异常,并说明您可能会遇到的一些棘手的类加载问题。
上课基础知识
本节描述了类加载的核心概念,以为本系列的其余部分提供知识库。
类加载器委托
类加载器委托模型是将加载请求彼此传递的类加载器的图。 引导类加载器位于该图的根部。 类加载器是由单个委托父级创建的,并在以下位置查找类:
- 快取
- 父母
- 自
类加载器首先确定过去是否曾要求它加载相同的类。 如果是这样,它将返回与上次返回的类相同的类(即,存储在缓存中的类)。 如果没有,它将给其父级加载该类的机会。 这两个步骤以递归和深度优先的方式重复。 如果父级返回null(或引发ClassNotFoundException
),则类加载器在其自己的路径中搜索类的源。
因为总是为父类加载器提供机会先加载一个类,所以该类由最接近根的类加载器加载。 这意味着所有核心引导程序类都由引导程序加载器加载,从而确保加载了正确版本的类,例如java.lang.Object
。 这还具有仅允许类加载器查看由其自身或其父级或祖先加载的类的效果。 它看不到其子级加载的类。
图1显示了三个标准类加载器:
图1.类加载器委托模型

与所有其他类加载器不同,引导类加载器(也称为原始类加载器)无法用Java代码实例化。 (通常,这是因为它本身是作为VM本身的一部分实现的。)此类加载器从引导类路径(通常是jre / lib目录中的JAR文件)加载核心系统类。 但是,您可以使用-Xbootclasspath
命令行选项(我们将在后面介绍)来修改-Xbootclasspath
路径。
扩展类加载器(也称为标准扩展类加载器)是引导类加载器的子级。 它的主要职责是从扩展目录(通常位于jre / lib / ext目录)中加载类。 这提供了简单地插入新扩展名(例如各种安全扩展名)的能力,而无需修改用户的类路径。
系统类加载器(也称为应用程序类加载器)是负责从CLASSPATH
环境变量指定的路径中加载代码的类加载器。 默认情况下,该类加载器是用户创建的任何类加载器的父级。 这也是ClassLoader.getSystemClassLoader()
方法返回的类加载器。
类路径选项
表1总结了用于设置三个标准类加载器的类路径的命令行选项:
命令行选项 | 说明 | 涉及类加载器 |
---|---|---|
-Xbootclasspath:< directories and zip/JAR files separated by ; or : > | 设置引导程序类和资源的搜索路径。 | 引导程序 |
-Xbootclasspath/a:< directories and zip/JAR files separated by ; or : > | 将路径追加到引导类路径的末尾。 | 引导程序 |
-Xbootclasspath/p:< directories and zip/JAR files separated by ; or : > | 将路径添加到引导类路径的前面。 | 引导程序 |
-Dibm.jvm.bootclasspath=< directories and zip/JAR files separated by ; or : > | 此属性的值用作附加搜索路径,该路径插入在-Xbootclasspath/p: 定义的任何值与引导类路径之间。 引导类路径可以是默认路径,也可以是-Xbootclasspath: 选项定义的路径。 | 引导程序 |
-Djava.ext.dirs=< directories and zip/JAR files separated by ; or : > | 指定扩展类和资源的搜索路径。 | 延期 |
-cp or -classpath < directories and zip/JAR files separated by ; or : > | 设置应用程序类和资源的搜索路径。 | 系统 |
-Djava.class.path=< directories and zip/JAR files separated by ; or : > | 设置应用程序类和资源的搜索路径。 | 系统 |
上课的阶段
一个类的加载本质上可以分为三个阶段:加载,链接和初始化。
大多数(如果不是全部)与类加载有关的问题可以追溯到在这些阶段之一中发生的问题。 因此,对每个阶段的透彻理解有助于诊断类加载问题。 这些阶段如图2所示:
图2.类加载的阶段

加载阶段包括查找所需的类文件(通过搜索相应的类路径)并加载字节码。 在JVM中,加载过程为类对象提供了非常基本的内存结构。 在此阶段不处理方法,字段和其他引用的类。 结果,该类不可用。
链接是三个阶段中最复杂的。 它可以分为三个主要阶段:
- 字节码验证。 类加载器对类的字节码进行大量检查,以确保其格式正确且行为良好。
- 上课准备。 此阶段准备必要的数据结构,这些数据结构表示每个类中定义的字段,方法和已实现的接口。
- 解决。 在此阶段,类加载器加载特定类引用的所有其他类。 可以通过多种方式引用这些类:
- 超类
- 介面
- 领域
- 方法签名
- 方法中使用的局部变量
在初始化阶段,将执行类中包含的所有静态初始化器。 在此阶段结束时,静态字段将初始化为其默认值。
在这三个阶段的最后,一个类已完全加载并可以使用。 请注意,可以以惰性方式执行类加载,因此,类加载过程的某些部分可以在首次使用类时完成,而不是在加载时完成。
显式与隐式加载
可以通过两种方式( 明确或隐式)加载类,而这两种加载方式之间存在细微的差异。
当使用以下方法调用之一加载类时,发生显式类加载:
-
cl .loadClass()
(其中cl
是java.lang.ClassLoader
的实例) -
Class.forName()
(起始类加载器是当前类的定义类加载器)
当调用这些方法之一时,由类加载器加载其名称指定为参数的类。 如果已经加载了该类,则只返回一个引用; 否则,加载程序将通过委托模型加载类。
当通过引用,实例化或继承(而不是通过显式方法调用)加载类时,就会发生隐式类加载。 在每种情况下,都在后台启动加载,并且JVM解析必要的引用并加载该类。 与显式类加载一样,如果该类已加载,则仅返回引用;否则,返回引用。 否则,加载程序将通过委托模型加载类。
通常通过显式和隐式类加载的组合来加载类。 例如,类加载器可以首先显式加载一个类,然后隐式加载其所有引用的类。
JVM的调试功能
上一节介绍了类加载的基本原理。 本节介绍了内置于IBM JVM中以帮助调试的各种功能。 其他JVM可能具有类似的调试功能。 有关详细信息,请参阅相应的文档。
详细输出
您可以使用-verbose
命令行选项打开IBM JVM的详细输出。 当某些事件发生时(例如,当类已加载时),详细输出将在控制台上显示信息。 有关其他类加载信息,可以使用详细的类输出。 使用-verbose:class
选项激活它。
解释详细输出
详细输出列出了所有已打开的JAR文件,并包括这些JAR的完整路径。 这是一个例子:
...
[Opened D:\jre\lib\core.jar in 10 ms]
[Opened D:\jre\lib\graphics.jar in 10 ms]
...
列出所有已加载的类,以及从中加载它们的JAR文件或目录。 例如:
...
[Loaded java.lang.NoClassDefFoundError from D:\jre\lib\core.jar]
[Loaded java.lang.Class from D:\jre\lib\core.jar]
[Loaded java.lang.Object from D:\jre\lib\core.jar]
...
详细的类输出显示其他信息,例如何时加载超类以及何时运行静态初始化程序。 一些示例输出如下:
...
[Loaded HelloWorld from file:/C:/myclasses/]
[Loading superclass and interfaces of HelloWorld]
[Loaded HelloWorldInterface from file:/C:/myclasses/]
[Loading superclass and interfaces of HelloWorldInterface]
[Preparing HelloWorldInterface]
[Preparing HelloWorld]
[Initializing HelloWorld]
[Running static initializer for HelloWorld]
...
详细的输出还显示了一些内部引发的异常(如果发生),包括堆栈跟踪。
使用-verbose解决问题
详细的输出可以帮助解决类路径问题,例如未打开JAR文件(因此不在类路径上)以及从错误的位置加载类。
IBM Verbose类装入
了解类加载器在哪里寻找类以及哪个类加载器加载特定类通常很有用。 您可以使用IBM Verbose类装入命令行选项获得此信息: -Dibm.cl.verbose=<class name>
。 您可以使用正则表达式来声明类的名称; 例如, Hello*
跟踪名称以Hello
开头的任何类。
只要用户定义的类加载器直接或间接扩展java.net.URLClassLoader
,则此选项也适用。
解释IBM Verbose Class Loading输出
IBM Verbose类装入输出显示了尝试装入指定类的类装入器及其外观。 例如,假设我们使用以下命令行:
java -Dibm.cl.verbose=ClassToTrace MainClass
在这里, MainClass
在其main方法中引用ClassToTrace
。 这将产生类似于此处输出的输出。
MainClass在其main方法中引用ClassToTrace
ExtClass loader attempting to find ClassToTrace
ExtClass loader using classpath D:\jre\lib\ext\gskikm.jar;D:\jre\lib\ext\ibmjceprovider.jar;
D:\jre\lib\ext\indicim.jar;
D:\jre\lib\ext\jaccess.jar;D:\jre\lib\ext\ldapsec.jar;
D:\jre\lib\ext\oldcertpath.jar
ExtClass loader could not find ClassToTrace.class in D:\jre\lib\ext\gskikm.jar
ExtClass loader could not find ClassToTrace.class in D:\jre\lib\ext\ibmjceprovider.jar
ExtClass loader could not find ClassToTrace.class in D:\jre\lib\ext\indicim.jar
ExtClass loader could not find ClassToTrace.class in D:\jre\lib\ext\jaccess.jar
ExtClass loader could not find ClassToTrace.class in D:\jre\lib\ext\ldapsec.jar
ExtClass loader could not find ClassToTrace.class in D:\jre\lib\ext\oldcertpath.jar
ExtClass loader could not find ClassToTrace
AppClass loader attempting to find ClassToTrace
AppClass loader using classpath C:\tests;D:\lib\tools.jar
AppClass loader could not find ClassToTrace.class in C:\tests
AppClass loader could not find ClassToTrace.class in D:\lib\tools.jar
AppClass loader could not find ClassToTrace
Exception in thread "main" java.lang.NoClassDefFoundError: ClassToTrace at MainClass(MainClass.java:6)
由于标准委派模型的工作方式,类加载器在父母之前列在孩子之前:父母优先。
注意,引导类加载器没有输出。 仅为扩展java.net.URLClassLoader
类加载器生成输出。 还要注意,类加载器在其类名称下列出; 如果有一个类加载器的两个实例,则可能无法区分它们。
使用IBM Verbose类加载解决问题
IBM Verbose类装入选项是检查所有类装入器的类路径已设置为什么的好方法。 它还指示哪个类加载器加载给定的类以及从何处加载它。 这样可以很容易地查看是否正在加载正确版本的类。
Java转储
Javadump (也称为Javacore )是您可能会发现有用的另一个IBM诊断工具。 要了解更多信息,请参阅《 IBM诊断指南》( 有关链接,请参阅参考资料 )。 发生以下事件之一时,JVM将生成Javadump:
- 发生致命的本机异常
- JVM堆空间不足
- 信号发送到JVM(例如,如果在Windows上按下Control-Break,在Linux上按下Control- \)
-
com.ibm.jvm.Dump.JavaDump()
方法被称为
触发Javadump的那一刻,详细信息就会记录在当前工作目录中保存的带日期戳的文本文件中。 该信息包括有关线程,锁,堆栈等的数据,以及有关系统中类加载器的丰富信息集。
解释Javadump的类加载部分
Javadump文件中提供的确切信息取决于运行JVM的平台。 类加载器部分包括:
- 定义的类加载器及其之间的关系
- 每个类加载器加载的类列表
以下是从Javadump获取的类加载器信息的快照:
CL subcomponent dump routine
============================
Classpath Z(D:\jre\lib\core.jar),...
Oldjava mode false
Bootstrapping false
Verbose class dependencies false
Class verification VERIFY_REMOTE
Namespace to classloader 0x00000000
Start of cache entry pool 0x44D85430
Start of free cache entries 0x44D86204
Location of method table 0x44C23AA0
Global namespace anchor 0x00266894
System classloader shadow 0x00376068
Classloader shadows 0x44D7BA60
Extension loader 0x00ADB830
System classloader 0x00ADB7B0
Classloader summaries
12345678: 1=primordial,2=extension,3=shareable,4=middleware,
5=system,6=trusted,7=application,8=delegating
-----ta- Loader sun/misc/Launcher$AppClassLoader(0x44D7BA60),
Shadow 0x00ADB7B0,
Parent sun/misc/Launcher$ExtClassLoader(0x00ADB830)
Number of loaded classes 1
Number of cached classes 260
Allocation used for loaded classes 1
Package owner 0x00ADB7B0
-xh-st-- Loader sun/misc/Launcher$ExtClassLoader(0x44D71288),
Shadow 0x00ADB830,
Parent *none*(0x00000000)
Number of loaded classes 0
Number of cached classes 0
Allocation used for loaded classes 3
Package owner 0x00ADB830
p-h-st-- Loader *System*(0x00376068), Shadow 0x00000000
Number of loaded classes 304
Number of cached classes 304
Allocation used for loaded classes 3
Package owner 0x00000000
ClassLoader loaded classes
Loader sun/misc/Launcher$AppClassLoader(0x44D7BA60)
HelloWorld(0x00ACF0E0)
Loader sun/misc/Launcher$ExtClassLoader(0x44D71288)
Loader *System*(0x00376068)
java/io/WinNTFileSystem(0x002CD118)
java/lang/Throwable(0x002C03A8)
java/lang/IndexOutOfBoundsException(0x44D45208)
java/lang/UnsatisfiedLinkError(0x44D42D38)
....................classes left out to save space........................
[Ljava/lang/Class;(0x002CA9E8)
java/io/InputStream(0x002C9818)
java/lang/Integer$1(0x002C83E8)
java/util/Dictionary(0x002C4298)
在此示例中,只有三个标准类加载器:
- 系统类加载器(
sun/misc/Launcher$AppClass loader
) - 扩展类加载器(
sun/misc/Launcher$ExtClass loader
) - 引导类加载器(
*System*
)
“类加载器摘要”部分提供有关系统中每个类加载器的详细信息,包括类加载器的类型。 在本系列文章中,关注的类型是原始,扩展,系统,应用和委托 (用于反思)。 持久可重用JVM中使用了其他类型( 可共享,中间件和可信任 ),这超出了本文的范围(有关更多信息,请参阅《持久可重用JVM用户指南》;下面的“ 相关主题”部分中有一个链接)。 摘要部分还显示了父类加载器:系统类加载器的父代是sun/misc/Launcher$ExtClass loader(0x00ADB830)
。 该父地址对应于父类加载器的本机数据结构(称为shadow )。
ClassLoader加载的类部分列出了每个类加载器加载的类。 在此示例中,系统类加载器仅加载了一个类HelloWorld
(位于地址0x00ACF0E0
)。
使用Javadump解决问题
使用Javadump中提供的信息,可以确定系统中存在哪些类加载器。 这包括任何用户定义的类加载器。 从已加载类的列表中,可以找出哪个类加载器已加载特定类。 如果找不到该类,则意味着该类未被系统中存在的任何类加载器加载(通常会导致ClassNotFoundException
)。
使用Javadump可以诊断的其他类型的问题包括:
- 类加载器名称空间问题。 类加载器名称空间是类加载器及其已加载的所有类的组合。 例如,如果存在一个特定的类,但由错误的类加载器加载(有时会导致
NoClassDefFoundError
),则名称空间不正确-也就是说,该类位于错误的类路径上。 要纠正此类问题,请尝试将类放置在其他位置(例如,在常规Java类路径中),并确保由系统类加载器加载该类。 - 类加载器约束问题。 我们将在本系列的最后一篇文章中讨论这种问题的示例。
Java方法跟踪
IBM JVM具有内置的方法跟踪工具。 这允许在不修改代码的情况下跟踪任何Java代码(包括核心系统类)中的方法。 由于此功能可以提供大量数据,因此您可以控制跟踪级别,以将所需信息归零。
启用跟踪的选项因JVM的发行版而异。 有关这些选项的详细信息,请参阅《 IBM诊断指南》( 有关链接,请参阅参考资料)。
以下是一些示例命令行:
要在IBM Java 1.4.2中运行HelloWorld
时跟踪所有java.lang.ClassLoader
方法:
java -Dibm.dg.trc.methods=java/lang/ClassLoader.*() -Dibm.dg.trc.print=mt HelloWorld
要跟踪ClassLoader
的loadClass()
方法和HelloWorld
的方法,以及IBM Java 1.4.2中的方法:
java -Dibm.dg.trc.methods=java/lang/ClassLoader.loadClass(),HelloWorld.*()
-Dibm.dg.trc.print=mt HelloWorld
解释方法跟踪输出
这是方法跟踪输出的示例(使用上一部分的第二个命令行)。
方法跟踪输出样本
...
> java/lang/ClassLoader.loadClass Bytecode method, This = 0x00D2B7B0, Signature: (Ljava/lang/String;)Ljava/lang/Class;
> java/lang/ClassLoader.loadClass Bytecode method, This = 0x00D2B7B0, Signature: (Ljava/lang/String;Z)Ljava/lang/Class;
> java/lang/ClassLoader.loadClass Bytecode method, This = 0x00D2B830, Signature: (Ljava/lang/String;Z)Ljava/lang/Class;
< * java/lang/ClassLoader.loadClass Bytecode method, looking for matching catch block for java.lang.ClassNotFoundException
< java/lang/ClassLoader.loadClass Bytecode method
< java/lang/ClassLoader.loadClass Bytecode method
> HelloWorld.main Bytecode static method, Signature: ([Ljava/lang/String;)V
< HelloWorld.main Bytecode static method
跟踪的每一行都提供了比上面显示的更多的信息。 让我们完整地看一看上面的几行:
12:57:41.277 0x002A23C0 04000D > java/lang/ClassLoader.loadClass Bytecode method,
This = 0x00D2B830, Signature: (Ljava/lang/String;Z)Ljava
/lang/Class;
此跟踪包括:
-
12:57:41.277
:方法进入或退出的时间戳。 -
0x002A23C0
:线程ID。 -
04000D
:一些高级诊断程序使用的内部JVM跟踪点。 - 其余信息显示是正在输入方法(
>
)还是已退出方法(<
),后面是该方法的详细信息。
使用方法跟踪解决问题
方法跟踪可用于解决不同类型的问题,包括:
- 性能热点:使用时间戳,可以找到需要花费大量时间才能执行的方法。
- 挂起:最后一个方法条目通常可以很好地指示应用程序挂起的位置。
- 错误的对象:使用该地址,可以通过与该对象的构造函数调用上的地址匹配来检查是否在所需对象上调用了方法。
- 意外的代码路径:通过跟踪入口和出口点,可以查看程序采用的任何意外的代码路径。
- 其他故障:通常最后输入的方法可以很好地指示发生故障的位置。
下一步是什么
在本文中,您了解了JVM中类加载的基础知识以及IBM JVM中可用的调试功能。 在本系列的下一篇文章中,您将学习如何应用这些知识来理解和解决运行Java应用程序时通常遇到的各种类加载问题。
翻译自: https://www.ibm.com/developerworks/java/library/j-dclp1/index.html