原文地址:http://ifeve.com/classloader%E8%A7%A3%E6%83%91/
感谢原作者:加多
一、什么是Classloader
一个Java程序要想运行起来,首先需要经过编译生成 .class文件,然后创建一个运行环境(jvm)来加载字节码文件到内存运行,而.class 文件是怎样被加载中jvm 中的就是Java Classloader所做的事情。
那么.class文件什么时候会被类加载器加载到jvm中运行那?比如执行new操作时候,当我们使用Class.forName(“包路径+类名”),Class.forName(“包路径+类名”,classloader),classloader.loadclass(“包路径+类名”);时候就触发了类加载器去类加载对应的路径去查找*.class,并创建Class对象。
二、Java自带的Classloader
2.1 BootstrapClassloader
引导类加载器,又称启动类加载器,是最顶层的类加载器,主要用来加载Java核心类,如rt.jar、resources.jar、charsets.jar等,Sun的JVM中,执行java的命令中使用-Xbootclasspath选项或使用- D选项指定sun.boot.class.path系统属性值可以指定附加的类,它不是 java.lang.ClassLoader的子类,而是由JVM自身实现的该类c 语言实现,Java程序访问不到该加载器。通过下面代码可以查看该加载器加载了哪些jar包。
public static void main(String[] args) {
// TODO Auto-generated method stub
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
}
输出:
file:/C:/Java/jdk1.8.0_121/jre/lib/resources.jar
file:/C:/Java/jdk1.8.0_121/jre/lib/rt.jar
file:/C:/Java/jdk1.8.0_121/jre/lib/sunrsasign.jar
file:/C:/Java/jdk1.8.0_121/jre/lib/jsse.jar
file:/C:/Java/jdk1.8.0_121/jre/lib/jce.jar
file:/C:/Java/jdk1.8.0_121/jre/lib/charsets.jar
file:/C:/Java/jdk1.8.0_121/jre/lib/jfr.jar
file:/C:/Java/jdk1.8.0_121/jre/classes
写到这里大家应该都知道,我们并没有在classpath里面指定这些类的路径,为啥还是能被加载到jvm并使用起来了吧,因为这些是bootstarp来加载的。
2.2 ExtClassloader
扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar包或者由java.ext.dirs系统属性指定的jar包。放入这个目录下的jar包对所有AppClassloader都是可见的(后面会知道ExtClassloader是AppClassloader的父加载器)。那么ext都是在那些地方加载类内:
System.out.println(System.getProperty("java.ext.dirs"));
C:\Java\jdk1.8.0_121\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
2.3 AppClassloader
系统类加载器,又称应用加载器,本文说的SystemClassloader和APPClassloader是一个东西,它负责在JVM启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH操作系统属性所指定的JAR类包和类路径。调用ClassLoader.getSystemClassLoader()可以获取该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器,这点通过ClassLoader的无参构造函数可以知道如下:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
System.out.println(System.getProperty("java.class.path"));
2.4 三种加载器联系
用一张图来表示三张图的关系如下:
用户自定义的无参加载器的父类加载器默认是AppClassloader加载器,而AppClassloader加载器的父加载器是ExtClassloader,验证代码如下:
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
输出:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null
根据输出可以看出:AppClassloader,ExtClassloader二者都是在sun.misc.Launcher
中定义.
一般我们都认为ExtClassloader的父类加载器是BootStarpClassloader,但是其实他们之间根本是没有父子关系的,只是在ExtClassloader找不到要加载类时候会去委托BootStrap加载器去加载。也就是知道上面输出父加载器为null.那么BootstrapClassLoader(bcl)去哪里加载?由于bcl需要在虚拟机启动的时候去加载Bootstrap Classes,所以bcl只能由虚拟机来实现了。HotSpot的bcl实现在classLoader.cpp。
2.5 类加载器原理
Java类加载器使用的是委托机制,也就是子类加载器在加载一个类时候会让父类来加载,那么问题来了,为啥使用这种方式那?因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。下面我们从源码看如何实现委托机制:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先从jvm缓存查找该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//然后委托给父类加载器进行加载
c = parent.loadClass(name, false);
} else {
//如果父类加载器为null,则委托给BootStrap加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
分析代码知道首先会执行(1)从jvm缓存查找该类,如何该类之前被加载过,则直接从jvm缓存返回该类,否者看当前类加载器是否有父加载器,如果有的话则委托为父类加载器进行加载(2),否者调用(3)委托为BootStrapClassloader进行加载,如果还是没有找到,则调用当前Classloader的findclass方法进行查找。从上面源码知道要想修改类加载委托机制,实现自己的载入策略 可以通过覆盖ClassLoader的findClass方法或者覆盖loadClass方法来实现。
2.6 Java中如何构造三种类加载器的结构
上面我们知道了加载顺序,既然虚拟机初始化的时候会使用scl来加载Main-Class,那我们以此为切入点看看java如何实现.
也就是说sun.misc.Launcher.getClassLoader()就是scl,那么看下sun.misc.Launcher
lacuncher的源码eclipse没找到。看下openjdk的,网上找的源码链接:
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);
}
}
}
上面分别创建了ecl.和根据ecl创建scl.下面是说默认下线程上下文加载器为appclassloader。
补充:
extcl = ExtClassLoader.getExtClassLoader();
loader = AppClassLoader.getAppClassLoader(extcl);
从上面可以看出来,AppClassLoader(scl)的parent是一个ExtClassLoader(ecl)实例
下面看下ExtClassLoader.getExtClassLoader()的代码
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);
SharedSecrets.getJavaNetAccess().
getURLClassPath(this).initLookupCache(this);
}
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;
}
上面代码:可知道两点:
1.ecl的路径:java.ext.dirs
2. 代码:super(getExtURLs(dirs), null, factory);
ExtClassLoader(ecl)并没有直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。
看下URLClassLoder的对应方法:
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
ucp = new URLClassPath(urls, factory);
acc = AccessController.getContext();
}
也就是明白了:ExtClassLoader的parent为null。
下面再看下
static class AppClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
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);
}
});
}
final URLClassPath ucp;
总结下Java应用启动过程是首先BootstarpClassloader加载rt.jar包里面的sun.misc.Launcher类,
而该类内部使用BootstarpClassloader加载器构建和初始化Java中三种类加载和线程上下文类加载器,
然后在根据不同场景去使用这些类加载器去自己的类查找路径去加载类(其中:Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分
,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用)。
另外:父加载类不是父类:从上面源码可知,ExtClassLoader和AppClassLoader同样继承自URLClassLoader
**************************分割线**********************************
上面是理论,下面自己动手写个例子实践一下。
3自定义ClassLoader
1 测试类
package com.daojia.test;public class Test {
public void hello(){
System.out.println("Hello world");
}
}
把编译完的class文件Test.class放到测试文件:e:\test
package com.daojia.load;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
private String classpath ;
public MyClassLoader(String path){
this.classpath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getFileName(name);
File file = new File(classpath,fileName);
byte[] data = getClassData(file);
if(data == null){
throw new ClassNotFoundException();
}
else{
return defineClass(name,data,0,data.length);
}
}
private byte[] getClassData(File file) {
try {
InputStream ins = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
byte[] data = baos.toByteArray();
ins.close();
baos.close();
return data;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index)+".class";
}
}
}
编写测试反射类:
package com.daojia.load;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClassLoadtest {
public static void main(String[] args) {
// TODO Auto-generated method stub
String classpath ="E:\\test\\";
//创建自定义类加载对象
MyClassLoader mloader = new MyClassLoader(classpath);
try {
//加载测试类
Class c = mloader.loadClass("com.daojia.test.Test");
if(c != null)
{
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("hello",null);
//通过反射调用Test类的hello方法
method.invoke(obj, null);
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
结果如下:
可以运行,说明自定义classload是OK