做者 某人Valar
如需转载请保留原文连接html本文涉及到的Java源码均为
Java8
版本java部分图片来自百度,若有侵权请联系删除c++
目录:缓存
前言:咱们刚刚接触Java时,在IDE(集成开发环境) 或者文本编辑器中所写的都是.java文件,在编译后会生成.class文件,又称字节码文件。安全
javac HelloWorld.java ---> HelloWorld.class 复制代码
对于.class文件来讲,须要被加载到虚拟机中才能使用,这个加载的过程就成为类加载。若是想要知道类加载的方式,就须要知道类加载器和双亲委托机制的概念。也就是咱们本篇所要介绍的内容。bash
Java中的类加载器能够分为两种:app
而系统类加载器又有3个:编辑器
Bootstrap ClassLoader用来加载JVM(Java虚拟机)
运行时所须要的系统类,其使用c++
实现。ide
从如下路径来加载类:oop
%JAVA_HOME%/jre/lib
目录,如rt.jar、resources.jar、charsets.jar等Java虚拟机的启动就是经过 Bootstrap ClassLoader建立一个初始类来完成的。 能够经过以下代码来得出Bootstrap ClassLoader所加载的目录:
public class ClassLoaderTest {
public static void main(String[]args) {
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
复制代码
打印结果为:
C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\classes
复制代码
能够发现几乎都是$JAVA_HOME/jre/lib
目录中的jar包,包括rt.jar、resources.jar和charsets.jar等等。
Extensions ClassLoader(扩展类加载器)具体是由ExtClassLoader
类实现的,ExtClassLoader
类位于sun.misc.Launcher
类中,是其的一个静态内部类。对于Launcher
类,能够先当作是Java虚拟机的一个入口。
ExtClassLoader
的部分代码以下:
Extensions ClassLoader负责将JAVA_HOME/jre/lib/ext
或者由系统变量-Djava.ext.dir
指定位置中的类库加载到内存中。
经过如下代码能够获得Extensions ClassLoader加载目录:
System.out.println(System.getProperty("java.ext.dirs"));
复制代码
打印结果为:
C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext
复制代码
也称为SystemAppClass(系统类加载器),具体是由AppClassLoader
类实现的,AppClassLoader
类也位于sun.misc.Launcher
类中。
部分代码以下:
经过如下代码能够获得App ClassLoader加载目录:
System.out.println(System.getProperty("java.class.path"));
复制代码
打印结果为:
C:\workspace\Demo\bin
复制代码
这个路径其实就是当前Java工程目录bin,里面存放的是编译生成的class文件。
在Java中,除了上述的3种系统提供的类加载器,还能够自定义一个类加载器。
为了能够从指定的目录下加载jar包或者class文件,咱们能够用继承java.lang.ClassLoader类的方式来实现一个本身的类加载器。
在自定义类加载器时,咱们通常复写findClass
方法,并在findClass
方法中调用defineClass
方法。
接下来会先介绍下ClassLoader类相关的具体内容,以后看一个自定义类加载器demo。
从上面关于ExtClassLoader、AppClassLoader源码图中咱们能够看到,他们都继承自URLClassLoader,那这个URLClassLoader是什么,其背后又有什么呢?
先来一张很重要的继承关系图:
关系:
还有2个结论:
咱们准备一个简单的demo 自建的一个Test.java文件。
public class Test{}
复制代码
public class Main {
public static void main(String[] args) {
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
}
}
复制代码
这样就能够获取到Test.class文件的类加载器,而后打印出来。结果是:
sun.misc.Launcher$AppClassLoader@75b83e92
复制代码
也就是说明Test.class文件是由AppClassLoader加载的。
那AppClassLoader是谁加载的呢? 其实AppClassLoader也有一个父加载器,咱们能够经过如下代码获取
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader);
loader = loader.getParent();
}
}
}
复制代码
上述代码结果以下:
sun.misc.Launcher$AppClassLoader@7565783b
sun.misc.Launcher$ExtClassLoader@1b586d23
复制代码
至于为什么没有打印出ExtClassLoader的父加载器Bootstrap ClassLoader,这是由于Bootstrap ClassLoader是由C++编写的,并非一个Java类,所以咱们没法在Java代码中获取它的引用。
上一节咱们看到了ClassLoader的getParent
方法,getParent
获取到的其实就是其父加载器。这一节将经过源码,来介绍ClassLoader中的一些重要方法。
ClassLoader类
---------
public final ClassLoader getParent() {
if (parent == null) return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
复制代码
咱们能够看到,其返回值有两种可能,为空
或者是parent
变量。
从源码中还能够发现其是一个final修饰的方法,咱们知道被final修饰的说明这个方法提供的功能已经知足当前要求,是不能够重写的, 因此其各个子类所调用的
getParent()
方法最终都会由ClassLoader来处理。
parent变量又是什么呢?咱们在查看源码时能够发现parent的赋值是在构造方法中。
ClassLoader类
---------
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
... //省略了无关代码
}
复制代码
而此构造方法又是私有的,不能被外部调用,因此其调用者仍是在内部。因而接着查找到了另外两个构造方法。
ClassLoader类
---------
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
复制代码
因此:
getSystemClassLoader()
方法的返回值。接着看上面代码中的getSystemClassLoader的源码:
ClassLoader类
---------
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
复制代码
其返回的是一个scl。在initSystemClassLoader()
方法中发现了对scl变量的赋值。
ClassLoader类
---------
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); //1
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
...//省略代码
}
sclSet = true;
}
}
复制代码
重点来了,注释1处其获取到的是Launcher
类的对象,而后调用了Launcher
类的getClassLoader()
方法。
Launcher类
---------
public ClassLoader getClassLoader() {
return this.loader;
}
复制代码
那这个this.loader是什么呢?在Launcher
类中发现,其赋值操做在Launcher
的构造方法中,其值正是Launcher
类中的AppClassLoader:
Launcher类
---------
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
...
}
复制代码
到这里谜团所有解开了:
在建立ClassLoder时,
getSystemClassLoader()
方法的返回值(也就是Launcher
类中的AppClassLoader)做为其parent。能将class二进制内容转换成Class对象,若是不符合要求的会抛出异常,例如ClassFormatError
、NoClassDefFoundError
。
在自定义ClassLoader时,咱们一般会先将特定的文件读取成byte[]对象,再使用此方法,将其转为class对象。
ClassLoader类
---------
/** * String name:表示预期的二进制文件名称,不知道的话,能够填null。 * byte[] b:此class文件的二进制数据 * int off:class二进制数据开始的位置 * int len:class二进制数据的总长度 */
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
复制代码
findClass()
方法通常被loadClass()
方法调用去加载指定名称类。
ClassLoader类
---------
/** * String name:class文件的名称 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
复制代码
经过源码看到ClassLoader类中并无具体的逻辑,而是等待着其子类去实现,经过上面的分析咱们知道两个系统类加载器ExtClassLoader
和AppClassLoader
都继承自URLClassLoader
,那就来看一下URLClassLoader
中的具体代码。
URLClassLoader类
---------
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 {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
...
return result;
}
private Class<?> defineClass(String name, Resource res) throws IOException {
...
URL url = res.getCodeSourceURL();
...
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
...
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
...
return defineClass(name, b, 0, b.length, cs);
}
}
复制代码
能够看到其对传入的name进行处理后,就调用了defineClass(name, res)
;在这个方法里主要是经过res资源和url,加载出相应格式的文件,最终仍是经过ClassLoader的defineClass
方法加载出具体的类。
上节说到findClass()
通常是在loadClass()
中调用,那loadClass()
是什么呢? 其实loadClass()
就是双亲委托机制的具体实现,因此在咱们先介绍下双亲委托机制后,再来分析loadClass()
。
先简单介绍下双亲委托机制: 类加载器查找Class(也就是在loadClass时)所采用的是双亲委托模式,所谓双亲委托模式就是
Bootstrap ClassLoader
。Bootstrap ClassLoader
能够从%JAVA_HOME%/jre/lib
目录或者-Xbootclasspath指定目录查找到,就直接返回该对象,不然就让ExtClassLoader
去查找。ExtClassLoader
就会从JAVA_HOME/jre/lib/ext
或者-Djava.ext.dir
指定位置中查找,找不到时就交给AppClassLoader
,AppClassLoader
就从当前工程的bin目录下查找CustomClassLoader
查找,具体查找的结果,就要看咱们怎么实现自定义ClassLoader的findClass
方法了。接下来咱们看看双亲委托机制在源码中是如何体现的。 先看loadClass的源码:
ClassLoader类
---------
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,根据name检查类是否已经加载,若已加载,会直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//若当前类加载器有父加载器,则调用其父加载器的loadClass()
c = parent.loadClass(name, false);
} else {
//若当前类加载器的parent为空,则调用findBootstrapClassOrNull()
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 1.若是到这里c依然为空的话,表示一直到最顶层的父加载器也没有找到已加载的c,那就会调用findClass进行查找
// 2.在findClass的过程当中,若是指定目录下没有,就会抛出异常ClassNotFoundException
// 3.抛出异常后,此层调用结束,接着其子加载器继续进行findClass操做
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
复制代码
findBootstrapClassOrNull()方法:能够看到其对name进行校验后,最终调用了一个native
方法findBootstrapClass()
。在findBootstrapClass()
方法中最终会用Bootstrap Classloader来查找类。
ClassLoader类
---------
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
private native Class<?> findBootstrapClass(String name);
复制代码
不会。 在Java中,咱们用包名+类名做为一个类的标识。 但在JVM中,一个类用其包名+类名和一个ClassLoader的实例做为惟一标识,不一样类加载器加载的类将被置于不一样的命名空间.
经过一个demo来看,
public class Main {
public static void main(String[] args) {
ClassLoaderTest myClassLoader = new ClassLoaderTest("F:\\");
ClassLoaderTest myClassLoader2 = new ClassLoaderTest("F:\\");
try {
Class c = myClassLoader.loadClass("com.example.Hello");
Class c2 = myClassLoader.loadClass("com.example.Hello");
Class c3 = myClassLoader2.loadClass("com.example.Hello");
System.out.println(c.equals(c2)); //true
System.out.println(c.equals(c3)); //flase
}
}
复制代码
输出结果:
true
false
复制代码
只有两个类名一致而且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类。
上面demo中用到的自定义ClassLoader:
自定义的类加载器
注意点:
1.覆写findClass方法
2.让其能够根据name从咱们指定的path中加载文件,也就是将文件正确转为byte[]格式
3.使用defineClass方法将byte[]数据转为Class对象
-------------
public class ClassLoaderTest extends ClassLoader{
private String path;
public ClassLoaderTest(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
byte[] classData = classToBytes(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
clazz= defineClass(name, classData, 0, classData.length);
}
return clazz;
}
private byte[] classToBytes(String name) {
String fileName = getFileName(name);
File file = new File(path,fileName);
InputStream in=null;
ByteArrayOutputStream out=null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length=0;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
return out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(in!=null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try{
if(out!=null) {
out.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
return null;
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
复制代码
到此Java的类加载器以及双亲委托机制都讲了个大概,若是文中有错误的地方、或者有其余关于类加载器比较重要的内容又没有介绍到的,欢迎在评论区里留言,一块儿交流学习。
下一篇会说道Java new一个对象的过程,其中会涉及到类的加载、验证,以及对象建立过程当中的堆内存分配等内容。