Java虚拟机类加载器详细介绍
Posted ShrMus
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java虚拟机类加载器详细介绍相关的知识,希望对你有一定的参考价值。
大家好,我是一个爱举铁的程序员Shr。
本篇文章将详细介绍类加载器,阅读完本篇文章你可能需要20分钟。
今天讲述的内容包括:类和类加载器之间的关系,类加载器的分类,类加载器的双亲委派模型。
一、什么是类加载器?
类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为类加载器[1]。
上一篇文章类加载机制中讲的是Java虚拟机如何加载Class文件,而类加载器就是实现“如何加载”的程序代码。
二、类和类加载器
类加载器只实现类的加载动作,而类在虚拟机中还会有链接,初始化等阶段,详细阶段请查看前一篇文章Java虚拟机(二)类加载机制。
对于一个类,都会由这个类本身和加载这个类的类加载器共同确认它在Java虚拟机中的唯一性。
举一个例子,一个类被不同的类加载器加载会发生什么。
新建一个Hello类,这个类将作为被不同类加载器加载的类。
package com.shrmus.classloader;
public class Hello {
}
新建一个自定义类加载器,继承java.lang.ClassLoader抽象类,重写findClass方法。
package com.shrmus.classloader;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name){
byte[] bs = null;
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream inputStream = getClass().getResourceAsStream(fileName);
if(inputStream == null) {
return super.loadClass(name);
}
bs = new byte[inputStream.available()];
inputStream.read(bs);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name, bs, 0, bs.length);
}
}
新建一个Main类。
package com.shrmus.classloader;
public class Main {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
Object newInstance = myClassLoader.findClass("com.shrmus.classloader.Hello").newInstance();
System.out.println(newInstance.getClass().getClassLoader());
System.out.println(newInstance instanceof com.shrmus.classloader.Hello);
Hello hello = new com.shrmus.classloader.Hello();
System.out.println(hello.getClass().getClassLoader());
System.out.println(hello instanceof com.shrmus.classloader.Hello);
}
}
控制台打印结果:
com.shrmus.classloader.MyClassLoader@33909752
false
sun.misc.Launcher$AppClassLoader@6d06d69c
true
在Main类中,用自定义的类加载器加载Hello类并生成实例。
然后第一行打印的是新生成的实例的类加载器。
第二行打印语句通过instanceof关键字来看新生成的实例是否还属于原来的类型,控制台打印的是false。
然后再用new关键字生成一个实例,第三行打印的是这个对象的类加载器,可以看到和第一行打印出来的类加载不一样。
第四行打印的是true。
这就说明了同一个类用不同的类加载器加载的话,即时原来的类型是一样的,最终在虚拟机中却被认为是不一样的。
三、类加载器的分类
Java中有三种类加载器,每个类加载器在创建时就加载它们对应的目录。
3.1 sun.misc.Launcher类
这个类用来启动主应用程序。
部分源代码如下:
/**
* This class is used by the system to launch the main application.
Launcher */
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader");
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader");
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
}
字段bootClassPath的值就是Bootstrap要加载的类库的路径。
System.out.println(System.getProperty("sun.boot.class.path"));
控制台打印结果:
D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\sunrsasign.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_152\jre\classes
它加载%Java_HOME%\jre\lib目录下的类库。
这个类的构造方法先是创建一个扩展类加载器。
然后再将扩展类加载器作为参数创建一个应用程序类加载器。
将应用程序类加载器设置成当前线程的上下文加载器。
最后,如果有需要,安装一个安全管理器。
3.2 扩展类加载器
扩展类加载器sun.misc.Launcher$ExtClassLoader,这个类是sun.misc.Launcher类的内部类。
源代码如下:
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {
private File[] dirs;
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
void addExtURL(URL url) {
super.addURL(url);
}
/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
this.dirs = dirs;
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
private static URL[] getExtURLs(File[] dirs) throws IOException {
Vector<URL> urls = new Vector<URL>();
for (int i = 0; i < dirs.length; i++) {
String[] files = dirs[i].list();
if (files != null) {
for (int j = 0; j < files.length; j++) {
if (!files[j].equals("meta-index")) {
File f = new File(dirs[i], files[j]);
urls.add(getFileURL(f));
}
}
}
}
URL[] ua = new URL[urls.size()];
urls.copyInto(ua);
return ua;
}
/*
* Searches the installed extension directories for the specified
* library name. For each extension directory, we first look for
* the native library in the subdirectory whose name is the value
* of the system property <code>os.arch</code>. Failing that, we
* look in the extension directory itself.
*/
public String findLibrary(String name) {
name = System.mapLibraryName(name);
for (int i = 0; i < dirs.length; i++) {
// Look in architecture-specific subdirectory first
// Read from the saved system properties to avoid deadlock
String arch = VM.getSavedProperty("os.arch");
if (arch != null) {
File file = new File(new File(dirs[i], arch), name);
if (file.exists()) {
return file.getAbsolutePath();
}
}
// Then check the extension directory
File file = new File(dirs[i], name);
if (file.exists()) {
return file.getAbsolutePath();
}
}
return null;
}
private static AccessControlContext getContext(File[] dirs)
throws IOException
{
PathPermissions perms =
new PathPermissions(dirs);
ProtectionDomain domain = new ProtectionDomain(
new CodeSource(perms.getCodeBase(),
(java.security.cert.Certificate[]) null),
perms);
AccessControlContext acc =
new AccessControlContext(new ProtectionDomain[] { domain });
return acc;
}
}
再看看sun.misc.Launcher类的构造方法中用ExtClassLoader.getExtClassLoader()创建一个扩展类加载器,而sun.misc.Launcher$ExtClassLoader类的getExtClassLoader()中第一条语句调用getExtDirs()方法。
getExtDirs()方法中第一条语句获取系统属性java.ext.dirs。
我们来看看这个系统属性是什么。
System.out.println(System.getProperty("java.ext.dirs"));
控制台打印结果:
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
它加载%JAVA_HOME%\jre\lib\ext目录中的类库。或者被java.ext.dirs系统变量指定的路径中的所有类库。
3.3 应用程序类加载器
应用程序类加载器sun.misc.Launcher$AppClassLoader,这个类也是sun.misc.Launcher类的内部类。
这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。
源代码如下:
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
// Note: on bugid 4256530
// Prior implementations of this doPrivileged() block supplied
// a rather restrictive ACC via a call to the private method
// AppClassLoader.getContext(). This proved overly restrictive
// when loading classes. Specifically it prevent
// accessClassInPackage.sun.* grants from being honored.
//
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
/*
* Creates a new AppClassLoader
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
}
/**
* Override loadClass so we can checkPackageAccess.
*/
public synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
return (super.loadClass(name, resolve));
}
/**
* allow any classes loaded from classpath to exit the VM.
*/
protected PermissionCollection getPermissions(CodeSource codesource)
{
PermissionCollection perms = super.getPermissions(codesource);
perms.add(new RuntimePermission("exitVM"));
return perms;
}
/**
* This class loader supports dynamic additions to the class path
* at runtime.
*
* @see java.lang.instrument.Instrumentation#appendToSystemClassPathSearch
*/
private void appendToClassPathForInstrumentation(String path) {
assert(Thread.holdsLock(this));
// addURL is a no-op if path already contains the URL
super.addURL( getFileURL(new File(path)) );
}
/**
* create a context that can read any directories (recursively)
* mentioned in the class path. In the case of a jar, it has to
* be the directory containing the jar, not just the jar, as jar
* files might refer to other jar files.
*/
private static AccessControlContext getContext(File[] cp)
throws java.net.MalformedURLException
{
PathPermissions perms =
new PathPermissions(cp);
ProtectionDomain domain =
new ProtectionDomain(new CodeSource(perms.getCodeBase(),
(java.security.cert.Certificate[]) null),
perms);
AccessControlContext acc =
new AccessControlContext(new ProtectionDomain[] { domain });
return acc;
}
}
再看sun.misc.Launcher类的构造方法中用AppClassLoader.getAppClassLoader(extcl)创建一个应用程序类构造器。
而sun.misc.Launcher$ExtClassLoader类的
getAppClassLoader()方法中获取系统属性java.class.path
我们来看看这个系统属性是什么。
System.out.println(System.getProperty("java.class.path"));
控制台打印结果:
D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\zipfs.jar;F:\workspace-eclipsej2ee\demo_20180611\bin
它加载用户类路径(ClassPath)指定的类库。如果应用程序没有自定义类加载器,这个就是程序默认的类加载器。
3.4 启动类加载器
启动类加载器在别的书中可能被称为引导类加载器,Bootstrap ClassLoader。
刚刚说了,sun.misc.Launcher类并不是启动类加载器,只是这个类创建了扩展类加载器和应用程序类加载器。
那sun.misc.Launcher类由哪个加载器来加载?
ClassLoader classLoader = sun.misc.Launcher.class.getClassLoader();
System.out.println(classLoader);
控制台打印结果:null
这个类的类加载器居然是null,为什么?
接下来就要讲讲类加载器的双亲委派模型你就知道原因了。
四、双亲委派模型
从Java虚拟机的角度来讲,只存在两种不同的类加载器,一种是启动类加载器,这个类加载器用C++语言实现,是虚拟机的一部分。一种是所有其他的类加载器,这些类加载器由Java语言实现,独立于虚拟机外部,全都继承抽象类java.lang.ClassLoader[1]。
但是从Java开发人员的角度来看,类加载器分为刚刚讲的那三种,启动类加载器,扩展类加载器,应用程序类加载器。
应用程序都是由这3种类加载器互相配合加载,还可以加入自己定义的类加载器。类加载器之间的关系如下图所示。
这种层次关系称为类加载器的双亲委派模型。这个模型要求除了顶层的启动类加载器之外,其余的类加载器都应该要有自己的父类加载器。
这里的类加载器之间的父子关系用组合关系来复用父加载器的代码,不是用继承关系。关于组合关系,我之前写过一篇设计模式(二) - UML类图有讲过组合关系,表示整体和部分的关系,部分不能脱离整体单独存在。
所以你再看sun.misc.Launcher类的构造方法,创建应用程序类加载器有一个参数是扩展类加载器。
你再仔细分析代码就会发现,是将扩展类加载器设置成应用程序类加载器的父加载器,而如果扩展类加载器创建没有成功的话,应用程序类加载器的父加载器就为null。
举个例子,新建一个Main类,看看它的加载器是哪一个。
package com.shrmus.classloader;
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Main.class.getClassLoader();
System.out.println(classLoader);
}
}
控制台打印结果:
sun.misc.Launcher$AppClassLoader@6d06d69c
Main类被应用程序类加载器加载,是预期的情况,然后打印应用程序类加载器的父加载器看看。
package com.shrmus.classloader;
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader appClassLoader = Main.class.getClassLoader();
System.out.println(appClassLoader);
ClassLoader parent = appClassLoader.getParent();
System.out.println(parent);
}
}
控制台打印结果:
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e
应用程序类加载器的父加载器是扩展类加载器,符合预期,那扩展类加载器的父加载器呢。
package com.shrmus.classloader;
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader appClassLoader = Main.class.getClassLoader();
System.out.println(appClassLoader);
ClassLoader ExtClassLoader = appClassLoader.getParent();
System.out.println(ExtClassLoader);
ClassLoader parent = ExtClassLoader.getParent();
System.out.println(parent);
}
}
控制台打印结果:
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e
null
前面两行都符合预期,但是打印扩展类加载器的父加载器却打印null。
根据双亲委派模型来看,其实扩展类加载器的父加载器应该是启动类加载器,但是Java用null表示启动类加载器。
那刚刚在3.4中sun.misc.Launcher类的加载器打印出来是null就能明白了吧,这个类是由启动类加载器加载。
此时,回到我举这个例子之前,也就是如果扩展类加载器创建没有成功的话,应用程序类加载器的父加载器就设置null了,就是启动类加载器,这样就不符合双亲委派模型了。
五、双亲委派模型的工作过程
如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才尝试自己去加载[1]。
六、双亲委派模型的实现
实现代码在java.lang.ClassLoader的loadClass()方法中,代码如下。
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 父加载器不为空,调用父加载器的findClass方法
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父加载器抛出异常说明父类加载器无法加载
// 调用本身的findClass方法来加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
先检查类是否已经被加载过,如果没有就调用父加载器的loadClass方法,如果父加载器为空就调用启动类加载器作为父加载器。
如果父加载器加载失败,再调用自己的findClass方法加载。
七、自定义类加载器
自定义类加载器需要继承java.lang.ClassLoader抽象类,建议重写findClass()方法。在前面讲类和类加载器时写了一个自定义类加载器,这里就不重复了。
总结
类加载器有三种,启动类加载器,扩展类加载器,应用程序类加载器。
类加载器的双亲委派模型,一个类被加载的时候会一层一层传送到顶层的启动类加载器,如果启动类加载器不能加载这个类,再一层一层传送给子类加载器加载。
关于类加载器的介绍就到这里了,后面有了更深的了解再来补充。
参考文献
[1] 周志明.深入理解Java虚拟机:JVM高级特性与最佳实践[M].机械工业出版社,2013
以上是关于Java虚拟机类加载器详细介绍的主要内容,如果未能解决你的问题,请参考以下文章