我们编写的Java程序得需要借助JVM才能运行起来,那么我们将程序编译成字节码之后,JVM是如何对字节码进 行加载的呢
类加载流程
我们自己编译后的代码在JVM加载的过程中会经历以下七个阶段:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
加载:当在代码中使用到某个类的时候,如果这个类没有被加载过,JVM就会将字节码文件读入
验证:在将字节码文件读入的时候会验证字节码里面的内容是否符合规则
准备:为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
初始化:初始化阶段就是调用构造方法的过程,并且还会为对类的静态变量初始化为指定的值,执行静态代码块
对于整体的加载流程如图所示:
加载方式
其实JVM对字节码的加载主要有两种方式,一种是显示加载,另外一种是隐式加载
显示加载就是我们自己手动的加载字节码文件,比如我们用Class.forName()来加载某个类的字节码
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.ssm.JVMDemo");
System.out.println(aClass);
}
当然除了用Class.forName()来加载,我们也可以用ClassLoader这个类来加载
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("com.ssm.JVMDemo");
System.out.println(aClass);
}
隐式加载就是不需要手动去加载,而是由JVM自己调用ClassLoader来加载
我们在启动JVM的过程中,其实JVM会自己会去加载一些核心的类,比如位于JDK包下的核心类库及自己编译好了的字节码文件,所以这就是为什么我们平时在运行某个程序的时候不需要手动去加载字节码文件
我们可以添加一个JVM的参数(-XX:+TraceClassLoading),打印下看看在JVM启动的过程中自己加载了哪些类
由于篇幅有限,我这里就只截取了一部分
这里关于隐式加载要注意一点,就是JVM并不是一开始就把所有的类加载进去,而是按需加载,就是当你使用到某个类的时候才会去加载
我们也可以通过程序来验证,先准备一个Java类代码
public class TestDemo { }
在上面我们是添加了一个JVM的参数,它可以实时的打印加载的类
public static void main(String[] args) {
TestDemo testDemo = new TestDemo();
}
在main方法里做了一个很简单的操作那就是 new 了一个TestDemo的对象,我们在 new 之前,控制台是没有打印加载这个类的信息的,但是在 new 完之后控制台打印了加载这个类的信息了
注意看我程序的断点是直接卡在这里,代码是没有执行的,此时控制台也没有打印加载这个类的信息
当我执行完这行代码,控制台就打印了加载这个类的信息了
类加载器
在上面就已经说了,不管是显示加载还是隐式加载都离不开ClassLoader这个类
ClassLoader这个类是一个抽象类,我们可以来看看这个类有哪些个实现类
这里最为主要的实现类就ExtClassLoader、AppClassLoader这两个,这两个类是Launcher类下的两个内部类,加载字节码文件的时候就是通过这两个加载器加载来加载的
但是这里每个类加载器就有着明确的分工,比如ExtClassLoader加载器就是主要加载JDK的ext包下的类,AppClassLoder主要加载我们自己编译的类
说到这里,那这两个类加载器又是谁来加载的呢,答案是引导类加载器---BootStrap
我们之前说过JVM在启动的时候会加载JDK核心包下的类,这两个类加载器就是位于核心包下
JVM在启动的时候会创建引导类加载器BootStrap,这个是由JVM来创建的,因此在JDK包下是看不到这个类的,因此到了这里一共讲了三个非常重要的类加载器BootStrap、ExtClassLoader、AppClassLoader,这三种类加载器的层级关系是这样的BootStrap-->ExtClassLoader-->AppClassLoader
双亲委派机制
说完类加载器,我们再看看看类加载器对于类的加载规则----双亲委派机制
类加载器在加载类的时候,为了防止重复加载的情况,JVM引入了双亲委派机制,所谓的双亲委派机制,就是当某个类加载器要去加载类的时候不会直接去加载,而是会委托父加载器加载,如果父加载器没法加载再由其子加载器加载
假如我们有一个自己编译的TestDemo类,最终加载的流程如下
而BootStrap加载器和ExtClassLoader加载器只加载JDK包下的类,因此我们自己编译的TestDemo类只能由AppClassLoader来加载
双亲委派机制为了防止重复加载之外,还保证了安全性,如果我们自己对JDK下的核心类库里的某个类进行修改,然后让AppClassLoader去加载的话,那么这必然会有安全隐患,这种委托机制就保证了核心类库只能由BootStrap或ExtClassLoader去加载
说完了整个类的加载流程我们再来看看源码
我们直接进入到AppClassLoader这类的loadClass方法里,给这个方法加一个断点调试条件
这个条件的意思就是当我们加载到TestDemo这个类的时候就会卡在这里
我们启动程序后,最终会来到这行代码
这里是直接调用了父类的loadClass方法,我们可以直接进入到这个方法里,最终会来到ClassLoder类里的loadClass方法,因为这个类最终是继承的ClassLoader这个类
来到这个类之后,这里会调用findLoadeClass方法,这句代码的意思就是判断当前的类加载器(AppClassLoader)有没有加载过此类,如果没有加载到的话就会直接调用父加载器的loadClass方法
AppClassLoader类加载器的父加载器是ExtClassLoader,而ExtClassLoader也同样是继承了ClassLoader,因此在调用loadClass的时候也同样会来到ClassLoader这个类里来
上一步我们调用parent.loadClass之后,再一次来到了这个ClassLoader的loadClass方法里,但是这个类的实例是ExtClassLoader,它也同样会去判断有没有加载过这个类,如果没有加载过的话就会直接调用findBootStrapClassOrNull这个方法
当我们点进去看的时候,这个方法是一个本地方法,最终会调用JVM底层的BootStrap加载器来加载这个类
因为BootsStrap类加载器只加载JDK核心包下的类,没法加载我们自己编译的类,因此这个方法返回的这个变量c一定是null
BootStrap加载器没有加载到,紧接着ExtClassLoader就会去加载,注意这里调用的findClass方法就是ExtClassLoader去尝试加载这个类
但是当我们运行这个方法的时候会发现这个方法会抛出个ClassNotFoundException
这个方法抛出错误就意味着ExtClassLoader也没有加载到这个类,紧接着就会来到AppClassLoader的loadClass方法里来
随然还是在ClassLoader类里,但是ClassLoader这个类引用的实例已经由ExtClassLoader变成了AppClassLoader
当AppClassLoader来调用findClass方法加载这个类的时候可以看到没有报错并且加载成功了
具体的代码流程图如下:
光用文字的形式来输出会比较的绕,大家可以按照这张流程图,自己断点调试一遍
打破双亲委派机制
这种双亲委派机制并不是定死,我们可以自己人为的打破它
看了源码我们都知道,不管是AppClassLoader还是ExtClassLoader都继承了ClassLoader这个类,最后都是调用了loadClass这个方法, 要想打破这种机制,我们只需要手动写一个类然后继承ClassLoader,重写loadClass,不让其去调用父加载器就行了
我们可以自己定义一个MyClassLoader,然后继承ClassLoader
package com.ssm;
import sun.misc.PerfCounter;
import java.io.FileInputStream;
public class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
String name1 = name.replaceAll("\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name1
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 当加载我们自己的类的时候我们就不委托父类去加载
if (!name.startsWith("com.ssm")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(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();
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
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();
}
}
}
注意这个重写的loadClass这个方法,我们并没有像ExtClassLoader和AppClassLoader那样去调用父加载器加载,而是直接去加载
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader myClassLoader = new MyClassLoader("D:\\");
Class<?> aClass = myClassLoader.loadClass("com.ssm.MybatisDemo", true);
System.out.println(aClass.getClassLoader());
}
这里注意加载的字节码文件是位于D盘下,它在加载这个字节码文件的时候就会直接去加载,而不是委托父加载器加载,这样我们就打破了双亲委派机制
那么这种打破双亲委派机制有什么用呢
我们在开发项目的时候通常是使用 tomcat 来部署,但是有时候一个 Tomcat 可能要部署多个项目,但是有可能每个项目依赖的外部jar包是一样的,但是版本有可能不一样,这时候打破双亲委派机制的好处就充分体现出来了,在 tomcat 启动的时候各自加载各自的外部依赖,多个项目之前互不干扰,避免了因包的冲突而造成的问题
具体如图所示