了解Java类加载
你想写ClassLoaders吗?或者你是否遇到
“意外的”ClassCastException或LinkageError以及
“加载器约束违规”的奇怪消息。那么,现在该仔细看看
Java类加载过程。
什么是ClassLoader以及它如何加载?
Java类由java.lang.ClassLoader 类的实例加载。
java.lang.ClassLoader本身是一个抽象类,所以一个类加载器只能有
一个java.lang.ClassLoader的具体子类的实例。如果是这种情况,哪个类加载器加载
java.lang.ClassLoader类本身?(经典的“谁将加载loader”引导问题)。
事实证明,JVM中内置了一个引导类加载器。引导
加载器加载java.lang.ClassLoader和许多其他Java平台类。
为了加载特定的Java类,比如说com.acme.Foo,JVM调用java.lang.ClassLoader的loadClass
方法(实际上,JVM寻找loadClassInternal方法 - 如果发现
它使用该方法,否则JVM使用loadClass。 loadClassInternal方法调用loadClass)。
loadClass接收要加载的类的名称并返回表示
加载的类的java.lang.Class实例。实际上,loadClass方法查找.class文件(或URL)的实际字节,并调用
defineClass
方法来构造byte数组外的java.lang.Class。在其上调用loadClass的Loader被
称为启动加载器(即,JVM使用该加载器启动加载)。但是,启动加载程序
不需要为类直接找到byte [],而是可以将类加载委派给
另一个类加载器(例如,它的父加载器) -
本身可以委托给另一个加载器等等。最后
,委托链中的一些类加载器对象最终调用defineClass方法来实际加载相关的类(com.acme.Foo)。
这个特别的程序叫定义装载机的com.acme.Foo。在运行时,Java类
由该对唯一标识 - 该类的完全限定名称以及加载它的定义加载器。
如果相同的命名(即相同的完全限定名称)类由两个不同的装载器定义,则这些类
是不同的 - 即使.class字节相同并从相同的位置(URL)加载。
多少个类加载器以及它们从哪里加载?
即使有一个很好的旧简单的“hello world”Java程序,也有至少3个类装载机。
引导加载程序
加载平台类(如java.lang.Object,java.lang.Thread等)
从rt.jar加载类(
JREHOME/lib/rt.jar)−Xbootclasspath可用于更改引导类路径−Xbootclasspath/p:和−Xbootclasspath/a:可用于预先添加/追加其他引导目录−这样做时非常谨慎。在大多数情况下,您希望避免使用引导类路径进行播放。在Sun的实现中,只读System属性sun.boot.class.path被设置为指向引导类路径。请注意,在运行时不能更改此属性−如果更改了无效的值。该加载器由Javanull表示。即,例如,java.lang.Object.class。getClassLoader()会返回null(所以对于其他引导类如java.lang.Integer,java.awt.Frame,java.sql.DriverManager等)扩展类加载器从已安装的可选包中加载类从
J
R
E
H
O
M
E
/
l
i
b
/
r
t
.
j
a
r
)
−
X
b
o
o
t
c
l
a
s
s
p
a
t
h
可
用
于
更
改
引
导
类
路
径
−
X
b
o
o
t
c
l
a
s
s
p
a
t
h
/
p
:
和
−
X
b
o
o
t
c
l
a
s
s
p
a
t
h
/
a
:
可
用
于
预
先
添
加
/
追
加
其
他
引
导
目
录
−
这
样
做
时
非
常
谨
慎
。
在
大
多
数
情
况
下
,
您
希
望
避
免
使
用
引
导
类
路
径
进
行
播
放
。
在
S
u
n
的
实
现
中
,
只
读
S
y
s
t
e
m
属
性
s
u
n
.
b
o
o
t
.
c
l
a
s
s
.
p
a
t
h
被
设
置
为
指
向
引
导
类
路
径
。
请
注
意
,
在
运
行
时
不
能
更
改
此
属
性
−
如
果
更
改
了
无
效
的
值
。
该
加
载
器
由
J
a
v
a
n
u
l
l
表
示
。
即
,
例
如
,
j
a
v
a
.
l
a
n
g
.
O
b
j
e
c
t
.
c
l
a
s
s
。
g
e
t
C
l
a
s
s
L
o
a
d
e
r
(
)
会
返
回
n
u
l
l
(
所
以
对
于
其
他
引
导
类
如
j
a
v
a
.
l
a
n
g
.
I
n
t
e
g
e
r
,
j
a
v
a
.
a
w
t
.
F
r
a
m
e
,
j
a
v
a
.
s
q
l
.
D
r
i
v
e
r
M
a
n
a
g
e
r
等
)
扩
展
类
加
载
器
从
已
安
装
的
可
选
包
中
加
载
类
从
JRE_HOME / lib / ext目录下的jar文件加载类
系统属性java.ext.dirs可能被设置为
使用-Djava.ext.dirs命令行选项更改扩展目录。
在Sun的实现中,这是sun.misc.Launcher $ ExtClassLoader的一个实例(实际上它是
sun.misc.Launcher类的内部类)。
以编程方式,您可以读取(只读!)系统属性java.ext.dirs以查找
哪些目录用作扩展目录。请注意,您无法
在运行时更改此属性 - 如果更改了无效的值。
应用类加载器
从应用程序类路径加载类
应用程序类路径使用
环境变量CLASSPATH(或)
带有Java启动器的-cp或-classpath选项
如果CLASSPATH和-cp都丢失,则“。” (当前目录)被使用。
只读System属性java.class.path具有
应用程序类路径的值。请注意,您无法在运行时更改此属性 -
如果更改了无效的值。
java.lang.ClassLoader.getSystemClassLoader()
返回这个加载器
这个加载器也被(称为“系统类加载器”)
(不会与加载Java“系统”类的引导加载器混淆)。
这是加载Java应用程序的“主”类(使用主要方法的类)的加载器。
在Sun的实现中,这是sun.misc.Launcher $ AppClassLoader的一个实例(实际上它是
sun.misc.Launcher类的内部类)。
默认的应用程序加载器使用扩展加载器作为其父装载器。
您可以通过命令行选项-Djava.system.class.loader更改应用程序类加载器。该值
指定java.lang.ClassLoader类的子类的名称。首先默认的应用程序
加载器加载命名的类(并且这个类必须在CLASSPATH或-cp中)并创建
它的一个实例。新创建的加载器用于加载应用程序主类。
典型的类装载流程
让我们假设你正在运行一个“hello world”java程序。我们将如何加载类。JVM使用
“应用程序类加载器” 加载主类。如果您运行以下程序
class Main {
public static void main(String[] args) {
System.out.println(Main.class.getClassLoader());javax.swing.JFrame f = new javax.swing.JFrame();
f.setVisible(true);SomeAppClass s = new SomeAppClass();
}
}
它打印类似的东西
sun.misc.Launcher$AppClassLoader@17943a4
每当必须从Main类中解析对其他类的引用时,JVM就会
使用Main类的定义加载器 - 应用程序类加载器 - 作为启动加载器。
在上面的示例中,要加载类javax.swing.JFrame,JVM将使用应用程序
类加载器作为启动加载器。即JVM将
在应用程序类加载器上调用loadClass()(loadClassInternal)。应用程序类加载器将其委托给扩展
类加载器。扩展类加载器检查它是否是引导类
(使用私有方法 - ClassLoader.findBootstrapClass),并且引导加载器
定义该类从rt.jar加载它。当必须解析对SomeAppClass的引用时,
JVM遵循相同的流程 - 它使用应用程序类加载器作为启动加载器
。应用程序加载器将其委托给扩展加载器。扩展加载程序使用
引导加载程序进行检查 Bootstrap加载程序不会找到“SomeAppClass”。然后扩展装载器检查
“SomeAppClass”是否在任何扩展器中,并且它不会找到任何扩展器。然后应用程序
类加载器检查应用程序CLASSPATH中的.class字节。如果发现,它定义
相同。如果不是,则会抛出NoClassDefFoundError。
概要
类是通过定义加载器和完全限定名来唯一标识的。
即使从
文件系统中相同位置的相同.class字节加载类,如果定义的加载程序不同,类也是不同的。
类加载器将加载委托给父装载器。
为了加载从“Bar”类引用的类“Foo”,JVM使用Bar的定义加载器
作为启动加载器。JVM将在Bar的定义加载器上调用loadClass(“Foo”)。
JVM缓存 - >每次
启动加载时的运行时类记录。JVM将在随后的解决方案中使用缓存。即不会
为每个引用调用loadClass 。这确保了时间不变 - 即,
不会允许为相同的类名加载不同的.class字节的ClassLoader 。它被
缓存保存。编写好的类加载器必须通过ClassLoader.findLoadedClass()
调用来检查缓存。
原文地址:https://blogs.oracle.com/sundararajan/understanding-java-class-loading