我们平常开发中,都会部署开发的项目或者本地运行main函数之类的来启动程序,那么我们项目中的类是如何被加载到JVM的,加载的机制和实现是什么样的,本文给大家简单介绍下。
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM,通过Java命令执行代码的大体流程如下
从流程图中可以看到类加载的过程主要是通过类加载器来实现的,那么什么是类加载器呢?
类加载器负责在运行时将Java类动态加载到JVM(Java 虚拟机)。此外,它们是JRE(Java运行时环境)的一部分。所以由于类加载器,JVM不需要知道底层文件或文件系统来运行Java程序。
Java类加载器的作用是寻找类文件,然后加载Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
它主要负责加载JDK内部类,一般是rt.jar和其他位于$JAVA_HOME/jre/lib目录下的核心库。此外,Bootstrap类加载器充当所有其他ClassLoader实例的父级。
Bootstrap ClassLoader是JVM核心的一部分,是用native引用编写的。它本身是虚拟机的一部分,所以它并不是一个JAVA类,我们无法直接使用该类加载器。
负责加载支撑JVM运行的位于$JAVA_HOME/jre/lib目录下的ext扩展目录中的JAR 类包。我们可以直接使用这个类加载器。
负责加载用户类路径(classpath)上的指定类库,主要就是加载你自己写的那些类。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
通过继承ClassLoader类实现,主要重写findClass方法。
下面通过代码来看下了解不同的类是使用的哪种类加载器来加载的:
System.out.println("Classloader of this class : " + ClassLoaderDrill.class.getClassLoader());
System.out.println("Classloader of Logging : " + Logging.class.getClassLoader());
System.out.println("Classloader of String : " + String.class.getClassLoader());
System.out.println("-----------");
System.out.println("Classloader : " + ClassLoaderDrill.class.getClassLoader());
System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent());
System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent().getParent());
下面是运行结果:
通过运行结果,我们会发现我自定义的当前运行类的类加载器是AppClassLoader,Logging这个类的类加载器是ExtClassLoader,而且类加载器之间是有父子关系关联的。但String的类加载器却为null,ExtClassLoader的父加载器也为null,是意味着String类不是通过类加载器加载的?那如果可以加载它又是怎么被加载的呢?为什么我们获取不到BootstrapClassLoader呢?后面我们会进行解读。
上面介绍了都有哪些类加载器,那么一个类是如何被类加载器加载的,这些类加载器之间又有什么关联关系呢,接下来就介绍下类加载器的机制。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它搜索的范围没有找到所需的类),子加载器才会尝试自己取加载。
双亲委派机制说简单点就是:对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。
参见最开始类运行加载全过程图可知,流程中会创建JVM启动器实例:sun.misc.Launcher。 sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。
在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。JVM默认使用Launcher的getClassLoader(),这个方法返回的类加载器(AppClassLoader)的实例加载我们的应用程序。
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);
}
Thread.currentThread().setContextClassLoader(this.loader);
。。。。。。 //省略一些不需关注代码
}
从上面Launcher构造方法的源码中,我们看到了AppClassLoader和ExtClassLoader这两种类加载器的定义,并且在创建AppClassLoader时将ExtClassLoader设置为父类,也符合上面说的类加载器之间的关联。
但是BootstrapClassLoader仍然没有出现,并且也没有给ExtClassLoader设置父加载器,那它又是和ExtClassLoader如何关联的?下面的双亲委派机制实现的源码会为我们解答。
我们来看下AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
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) {
// 解析、链接指定的Java类
resolveClass(c);
}
return c;
}
}
上面就是双亲委派机制实现原理的源码。从中我们可以看到有一个逻辑点会调用findBootstrapClassOrNull()这个方法,那么至此,我们有个疑团也就解开了:ExtClassLoader和BootstrapClassLoader(启动类加载器)就是在这里关联上的。因为ExtClassLoader在定义的时候,没有设置父类加载器(parent),所以执行到了这个逻辑,委托了BootstrapClassLoader进行加载。上面说的类加载器之间层级关系的实现和关联,也是在块逻辑里实现的。从源码这里的逻辑,也符合前面我们介绍BootstrapClassLoader所说的:Bootstrap类加载器充当所有其他ClassLoader实例的父级。
这个疑团是解开了,但是之前还有一个疑团仍然没有说明,在开始我们获取不同的类的加载器的时候,String的类加载器是null。在类加载的源码里面,我们看到了BootstrapClassLoader加载器的获取,为什么获取不到是null呢。这个我们要看下findBootstrapClassOrNull()这个方法的实现,看看BootstrapClassLoader到底是怎么定义的。
/**
* Returns a class loaded by the bootstrap class loader;
* or return null if not found.
*/
private Class> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// return null if not found
private native Class> findBootstrapClass(String name);
通过源码可以看到最终调用了findBootstrapClass这个方法来返回,但是这个方法的修饰符是native,那么就容易理解我们为什么获取不到这个BootstrapClassLoader了。
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改 ,防止了恶意代码的注入,安全性的提高和保障。
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。如果每个加载器都自己加载,那么可能会出现多个同名类,导致混乱。
双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。
若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码时,就需要破坏双亲委派模型了。下面就介绍几种破坏了双亲委派机制的场景。
JNDI是Java标准服务,它的代码由启动类加载器去加载,但JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。为了解决这个问题,引入了一个线程上下文类加载器(ContextClassLoader)。可通过Thread.setContextClassLoader()设置。利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。
Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。
那么Spring是如何访问WEB-INF下的用户程序呢?——使用线程上下文类加载器
Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。利用这个来加载用户程序,即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。
在介绍类加载器种类的时候,一共有四种,前面所说的都是前三种类加载器的一些机制,那如果我们想自己自定义个类加载器要如何实现呢?
自定义类加载器,只需继承ClassLoader抽象类,并重写findClass方法(如果要打破双亲委派模型,需要重写loadClass方法)。下面是个自定义类加载器的例子:
public class ClassLoaderDrill {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
protected Class> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
public static void main(String args[]) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//创建 /com/xxx/xxx 的几级目录,跟你要加载类的目录一致
Class clazz = classLoader.loadClass("com.test.jvm.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
注意:一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。这个在ClassLoader的构造方法实现里可以看到。
上述我们介绍了类加载器及相关机制和实现源码,但是类加载器获取所需要的类这个动作,只是类加载全过程中的一部分。类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析三个部分统称为链接,这7个阶段的发生顺序如图:
下面也给大家简单介绍下每个阶段所执行的具体动作
JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存(JVM)中,并生成一个代表该类的 java.lang.Class 对象。该阶段JVM完成3件事:
主要确保加载进来的字节流符合JVM规范。JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行,该阶段是保证 JVM 安全的重要屏障。
验证阶段会完成以下4个阶段的检验动作:
该步主要为静态变量在方法区分配内存,并设置默认初始值。JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的变量)分配内存并初始化。
虚拟机将常量池内的符号引用替换为直接引用的过程,即将常量池中的符号引用转化为直接引用。
在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。当使用阶段完成之后,java类就进入了卸载阶段。
关于类的卸载,在类使用完之后,如果满足下面的情况,jvm就会在方法区垃圾回收的时候对类进行卸载。类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
最后介绍了下类加载的整个过程及执行的具体动作,其实每个节点去深挖也是有很多内容的,感兴趣的小伙伴可以再去深入了解。