JAVA 双亲委派之一
JVM类加载流程
java语言系统内置了众多类加载器,从一定程度上讲,只存在两种不同的类加载器:一种是启动类加载器,此类加载由C++实现,是JVM的一部分;另一种就是所有其他的类加载器,这些类加载器均由java实现,且全部继承自java.lang.ClassLoader
- Bootstrap ClassLoader 启动类加载器,最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib目录中或-Xbootclasspath中参数指定的路径中的,并且是虚拟机识别的(按名称)类库
- Extention ClassLoader 扩展类加载器,由启动类加载器加载,实现为sun.misc.Launcher$ExtClassLoader,负责加载目录%JRE_HOME%/lib/ext目录中或-Djava.ext.dirs中参数指定的路径中的jar包和class文件
- Application ClassLoader 应用类加载器,也称为系统类加载器(System ClassLoader,可由java.lang.ClassLoader.getSystemClassLoader()获取),实现为sun.misc.Launcher$AppClassLoader,由启动类加载器加载,负责加载当前应用classpath下的所有类
双亲委派模型
java语言系统有众多类加载器,包括用户自定义类加载器,各加载器之间的加载顺序如何?首先从JVM入口应用sun.misc.Launcher聊起
Launcher
public Launcher() {
ExtClassLoader localExtClassLoader;
try {
// 加载扩展类加载器
localExtClassLoader = ExtClassLoader.getExtClassLoader();
} catch (IOException localIOException1) {
throw new InternalError("Could not create extension class loader", localIOException1);
}
try {
// 加载应用类加载器
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
} catch (IOException localIOException2) {
throw new InternalError("Could not create application class loader", localIOException2);
}
// 设置AppClassLoader为线程上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);
// ...
static class ExtClassLoader extends java.net.URLClassLoader
static class AppClassLoader extends java.net.URLClassLoader
}
Launcher初始化了ExtClassLoader和AppClassLoader,并将AppClassLoader设置为线程上下文类加载器,同时,初始化AppClassLoader时传入了ExtClassLoader实例,WHY? 这里要写一个大大的问号ExtClassLoader和AppClassLoader都继承自URLClassLoader,而最终的父类则为ClassLoader。
在这里插入图片描述
查看源码可以得知,初始化AppClassLoader时传入的ExtClassLoader实例最终设置为了AppClassLoader(ClassLoader)的parent属性,parent属性的作用是什么?
父类加载器
每个类都对应一个加载它的类加载器,我们可以通过程序来验证
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
System.out.println("DNSNameService's ClassLoader is " + DNSNameService.class.getClassLoader());
System.out.println("String's ClassLoader is " + String.class.getClassLoader());
}
}
输出为
ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
DNSNameService's ClassLoader is sun.misc.Launcher$ExtClassLoader@63c12fb0
String's ClassLoader is null
PathClassLoader为我们自己创建的类,其类加载器为AppClassLoader
DNSNameService为%JRE_HOME%/lib/ext目录下的类,其类加载器为ExtClassLoader
String存在于rt.jar中,但其类加载器为null,这里是应为rt.jar由Bootstrap ClassLoader加载,而Bootstrap ClassLoader是由C++编写,属于JVM的一部分
每个类加载器,都有一个父类加载器(parent),同样通过程序验证
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println("PathClassLoader's ClassLoader is " + PathClassLoader.class.getClassLoader());
System.out.println("PathClassLoader's ClassLoader is " + PathClassLoader.class.getClassLoader().getParent());
System.out.println("PathClassLoader's ClassLoader is " + PathClassLoader.class.getClassLoader().getParent().getParent());
}
}
输出:
PathClassLoader's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
PathClassLoader's ClassLoader is sun.misc.Launcher$ExtClassLoader@63c12fb0
PathClassLoader's ClassLoader is null
AppClassLoader的父类加载器为ExtClassLoader
ExtClassLoader的父类加载器为null,
源码解析
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
// 父类URLClassLoader方法
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory)
super方法中传参就null
null并不代表ExtClassLoader没有父类加载器,而是Bootstrap ClassLoader
双亲委派
类加载器在加载类或者其他资源时,使用的是如上图所示的双亲委派模型,这种模型要求除了顶层的BootStrap ClassLoader外,其余的类加载器都应当有自己的父类加载器(父类加载器不是父类继承),如果一个类加载器收到了类加载请求,首先会把这个请求委派给父类加载器加载,只有父类加载器无法完成类加载请求时,子类加载器才会尝试自己去加载。
要理解双亲委派,可以查看ClassLoader.loadClass方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) { // 没有被加载过
long t0 = System.nanoTime();
// 首先委派给父类加载器加载
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果父类加载器无法加载,才尝试加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
破坏双亲委派模型
双亲委派模型并不是一个强制性的约束模型,在java的世界中大部分的类加载器都遵循这个模型,但也有例外,一个典型的例子便是JNDI服务。
JNDI存放于rt.jar,由启动类加载器加载,JNDI的目的是对资源进行集中管理和查找,它需要调用由各厂商实现并部署在应用程序ClassPath下的JNDI接口实现的代码,如此一来,在双亲委派模型下,启动类加载器根本无法加载这些代码
针对此问题,java设计团队引入了线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过Thread类的setContextClassLoader进行设置,默认继承父线程类加载器
通过线程上下文类加载器,JNDI服务通过此类加载器,由父类加载器请求子类加载器完成类加载动作
破坏双亲委派模型的例子还有很多,如tomcat服务、osgi、jigsaw等等,是否破坏双亲委派模型并没有对与错,只是不同场景下的具体应用而已
自定义ClassLoader
不论是AppClassLoader还是ExtClassLoader还是启动类加载器,其加载类的路径都是固定的,如果我们需要加载外部类或者资源,如某路径下或网络上,这样便需要自定义类加载器
自定义类加载器,只需要继承ClassLoader类,复写findClass方法,在findClass方法中调用defineClass方法即可
一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader
如果需要制定一个ClassLoader的父类加载器为启动类加载器,只需要将其parent指定为null即可
首先,编写一个测试用的类文件
package com.lvyuanj.standard.test;
public class MyDefinedClass {
public void say() {
System.out.println("I'm MyDefinedClass by " + this.getClass().getClassLoader());
}
}
将其编译,放入D:\TestXf\classloader目录下接下来,
编写MyDefinedClassLoader
package com.lvyuanj.standard.test;
import java.net.URL;
import java.net.URLClassLoader;
public class MyDefinedClassLoader extends URLClassLoader {
public MyDefinedClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public MyDefinedClassLoader(URL[] urls) {
super(urls);
}
}
这里直接继承URLClassLoader类,该类findClass的实现如下
protected Class<?> findClass(final String name) throws ClassNotFoundException {
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 类文件全路径
String path = name.replace('.', '/').concat(".class");
// 指定资源目录下查找
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// 调用defineClass生成类
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
编写测试程序
package com.lvyuanj.standard.test;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
public class MyDefinedClassLoaderDemo {
public static void main(String[] args) throws Exception {
URL path = new File("D:\\TestXf\\classloader").toURI().toURL();
URL[] urls = new URL[1];
urls[0] = path;
MyDefinedClassLoader myDefinedClassLoaderA = new MyDefinedClassLoader(urls);
Class<?> aClass = myDefinedClassLoaderA.loadClass("com.lvyuanj.standard.test.MyDefinedClass");
Method sayA = aClass.getMethod("say");
Object instanceA = aClass.newInstance();
sayA.invoke(instanceA);
System.out.println(aClass.getClassLoader().toString());
System.out.println("aClass@"+aClass.hashCode());
System.out.println("====================");
MyDefinedClassLoader myDefinedClassLoaderB = new MyDefinedClassLoader(urls, myDefinedClassLoaderA);
Class<?> bClass = myDefinedClassLoaderB.loadClass("com.lvyuanj.standard.test.MyDefinedClass");
Method sayB = bClass.getMethod("say");
Object instanceB = bClass.newInstance();
sayB.invoke(instanceB);
System.out.println(myDefinedClassLoaderB);
System.out.println("bClass@"+bClass.hashCode());
System.out.println("====================");
MyDefinedClassLoader myDefinedClassLoaderC = new MyDefinedClassLoader(urls);
Class<?> cClass = myDefinedClassLoaderC.loadClass("com.lvyuanj.standard.test.MyDefinedClass");
Method sayC = cClass.getMethod("say");
Object instanceC = cClass.newInstance();
sayC.invoke(instanceC);
System.out.println(myDefinedClassLoaderC);
System.out.println("cClass@"+cClass.hashCode());
System.out.println("====================");
System.out.println("aClass == bClass :"+ (aClass == bClass));
System.out.println("cClass == bClass :"+ (cClass == bClass));
}
}
输出为
I'm MyDefinedClass by com.lvyuanj.standard.test.MyDefinedClassLoader@619a5dff
com.lvyuanj.standard.test.MyDefinedClassLoader@619a5dff
aClass@2117255219
====================
I'm MyDefinedClass by com.lvyuanj.standard.test.MyDefinedClassLoader@619a5dff
com.lvyuanj.standard.test.MyDefinedClassLoader@7ab2bfe1
bClass@2117255219
====================
I'm MyDefinedClass by com.lvyuanj.standard.test.MyDefinedClassLoader@497470ed
com.lvyuanj.standard.test.MyDefinedClassLoader@497470ed
cClass@186276003
====================
aClass == bClass :true
cClass == bClass :false
这里我们定义了3个ClassLoader,类搜索路径均为D:\TestXf\classloader,其中myDefinedClassLoaderB的父类加载器为myDefinedClassLoaderA
aClass bClass cClass 分别由 myDefinedClassLoaderA myDefinedClassLoaderB myDefinedClassLoaderC 加载
instanceA instanceB instanceC 分别由 aClass bClass cClass 创建
从输出可以看出,instanceA及instanceB所对应的class均由myDefinedClassLoaderA加载
虽然myDefinedClassLoaderA与myDefinedClassLoaderB为两个不同的类加载器,但由于myDefinedClassLoaderB的父类加载器为myDefinedClassLoaderA,从输出结果可以看出,aClass与bClass完全相同(包括内存地址),这也说明aClass与bClass均由同一类加载器加载
而instanceC所对应的class,cClass却是由myDefinedClassLoaderC加载
这个例子,能更好的帮助理解双亲委派模型
**注意:如果使用IDEA编辑器的话,一定不要创建MyDefinedClass 这个类,否则sun.misc.Launcher
A
p
p
C
l
a
s
s
L
o
a
d
e
r
默认的加载器。自定义
C
l
a
s
s
L
o
a
d
e
r
加载器,继承
C
l
a
s
s
L
o
a
d
e
r
,重写
f
i
n
d
C
l
a
s
s
指定路径,加载的类一定不要放到本工程目录对应的包下面,否则
f
i
n
d
C
l
a
s
s
都不会执行,如果
l
o
a
d
C
l
a
s
s
(
"
包名
+
类名
"
),此类会被
s
u
n
.
m
i
s
c
.
L
a
u
n
c
h
e
r
AppClassLoader默认的加载器。 自定义ClassLoader加载器,继承ClassLoader,重写findClass 指定路径,加载的类一定不要放到本工程目录对应的包下面,否则findClass都不会执行,如果loadClass("包名+类名"),此类会被sun.misc.Launcher
AppClassLoader默认的加载器。自定义ClassLoader加载器,继承ClassLoader,重写findClass指定路径,加载的类一定不要放到本工程目录对应的包下面,否则findClass都不会执行,如果loadClass("包名+类名"),此类会被sun.misc.LauncherAppClassLoader默认加载器加载,是可以直接找到此类,导致自定义的findClass不执行。 使用loadClass(“类名”), 找不到才会再执行findClass方法。正好次方法验证了类加载向父类查找建议:需要加载的类,使用IDEA 写好之后,然后把文件copy到其它目录,然后删除本工程下面的的文件
**
类的唯一性
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间如上例,虽然classA(classB)与classC的类路径相同,但由于被不同的类加载器加载,其却属于两个不同的类名称空间
手动打包jar
package com.lvyuanj.standard.model;
public class StudentA {
private String name;
private Integer age;
private String address;
public StudentA() {
System.out.println("--------------->StudentA constructor-----------");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void print(){
System.out.println("---------------------> excute print method");
}
}
package com.lvyuanj.standard.model;
public class StuMain {
public static void main(String[] args) {
StudentA studentA = new StudentA();
studentA.print();
}
}
手动编码
- 执行: javac -encoding utf-8 -d classes com/lvyuanj/standard/model/*.java
打jar包
- 创建MANIFEST.MF文件
Manifest-Version: 1.0
Main-Class: com.lvyuanj.standard.model.StuMain
请务必在文件的最后一行,至少加一个回车换行
Manifest-Version 版本号默认为1.0
Main-Class 入口的主类,也就是main方法所在的类
Class-Path 引用外部类的路径
2.打包
在该文件夹下执行:jar -cvfm stu.jar MANIFEST.MF com
jar命令是jdk自带的,可在%JAVA_HOME%\bin中找到
c 创建新档案
v 在标准输出中生成详细输出,有输出信息
f 指定档案文件名
m 包含指定清单文件中的清单信息,也就是指定我们的MANIFEST.MF 如果没有这个参数,会生成一个默认的MANIFEST.MF文件
stu.jar 指定生成jar包的名称
MANIFEST.MF com commons-lang3-3.4.jar 要打包的文件
成功之后会在当前文件夹下生成一个stu.jar
- 运行:
执行命令: java -jar stu.jar
输出结果:
--------------->StudentA constructor-----------
---------------------> excute print method
验证ExtClassLoader加载
把以上打包好的stu.jar 包放到 jre/lib/ext 目录下面
public static void main(String args[]) {
Class<?> classz = Class.forName("com.lvyuanj.standard.model.StudentA");
System.out.println("classload :"+classz.getClassLoader());
}
结果:
classload:sun.misc.Launcher$ExtClassLoader@63c12fb0