JVM类的加载相关的介绍
学习类的加载的加载过程对深入理解JVM有十分重要的作用,下面就跟我一起学习JVM类的加载过程吧!
文章目录
- JVM类的加载相关的介绍
- 一、类的加载过程
- 二、双亲委派机制
- 1、类加载器的种类
- 2、为什么JVM要分成不同的类的加载器
- 3、类的加载过程
- 4、为什么要通过双亲委派机制
- 5、从代码层次分析双亲委派机制
- 三、自定义类加载器
- 四、打破双亲委派机制
- 1.tomcat打破双亲委派机制
- 2.SPI 打破双气双亲委派机制
一、类的加载过程
JVM的类的加载过程分为五个阶段:加载、验证、准备、解析、初始化
加载
加载阶段就是将编译好的的class文件通过字节流的方式从硬盘或者通过网络加载到JVM虚拟机当中来。(我们平时在Idea中书写的代码就是放在磁盘中的,也可以通过网络加载到虚拟机)
验证
验证阶段主要是验证class文件的格式或者内容是否符合java规范和虚拟机规范,比如最常见的字节码文件中的cafe baby,以及通过class文件是u2或者u4的的方式解析字节码中各部分的内容是否符合规范。
准备
准备阶段就是给静态变量(static修饰的变量)赋默认值的过程,int类型的赋值为0,对象类型赋值为null,这都是默认值,需要注意的是常量,也就是被final修饰的static,在类编译的时候就已经赋值完成了。
解析
解释阶段做的事情用一句话来说就是将符号引用转变为直接引用(句柄)
句柄:如下如所示虚拟机有两种引种对象数据的方式,一种是通过句柄池的方式,一种是通过直接指针的方式,句柄池的方式就是在引用和真实地址之间多了一层句柄,这样的好处就是当引用的真实对象发生改变时,栈中存放的值不需要改动,但是多了一层就会增加系统的复杂性,所以hotsport虚拟机用的是直接地址指针的方式(如下图)。
直接引用:数据在内存中的真实地址
符号引用: 包括一些方法名和字面量等,这些符号引用在加载过程中有的可以确定下来的,比如static修饰的main方法,这些可以确定下来的引用在解析过程中就会转变为指向真实地址的指针,还有一些是只有在实际运行期间才可以确定下来的,比如多态,接口等,这些就需要到真正调用的时候才转为直接地址引用。(这也就是静态连接和动态连接的区别)
初始化
类的初始化是将给静态变量赋值(这时候的赋值才是赋代码中书写的值)静态代码块收集在一起进行执行的过程(其实就是执行字节码中的clinit方法,注意这个方法不是类的构造方法)执行顺序根据静态变量和静态代码块在类中的顺序决定。代码如下
public class Hello {
//准备阶段给a赋值为0.初始化的时候给a赋值为3
public static int a=3;
//根据顺序先执行a=3,在实行静态方法
static {
String b="b";
}
public static void main(String[] args) {
}
}
注意:类的初始化,实例化对象
二、双亲委派机制
双亲委派机制是我们学习类的加载和面试过程中逃不掉的问题,接下来我会通过介绍类加载器的种类,jvm虚拟机使通过什么方式调用这些类加载器完成类的加载的,双亲委派机制是什么,打破双亲委派机制的例子有哪些。
1、类加载器的种类
类的加载过程,同通过类加载器实现的。类的加载器主要有如下几种:
引导类加载器:c++代码实现,负责接在java的核心类库,比如rt.jar等
扩展类加载器:加载java的一些扩展类,负责加载jre下ext扩展目录下的jar报
应用类加载器:负责加载classpath下的类,也就是我们自己写的类
自定义类加载器:我们自己写的写的类加载器,负责加载自定义路径下的类
2、为什么JVM要分成不同的类的加载器
介绍为什么要搞这么多类加载器之前,就需要先说明JVM是如何使用这些类加载器的(也就是双亲委派机制)。双亲委派机制就是(引导类加载器->扩展器类加载器->应用类加载器->自定义类加载器 逐层上层是下层类加载器的父亲,注意这里不同于extends的继承关系,是类加载器中有一个parent属性,指向它的父亲是谁)
3、类的加载过程
抛开自定义加载器先不谈,当我们自己编的类加载的时候,会先去应用类加载器中查找该类是否已经加载了(如果加载了直接返回),如果没有则向上委托扩展类加载器看它是否已经加载(加载了直接返回),如果没有则继续向上委托(加载了直接返回),若没有加载并且是需要加载核心JAVA的核心类库,则加载如果不是核心类库,则通知下层去加载,如果是JAVA的扩展包的类就通知扩展类加载器加载,如果是用户自定义的类就再由扩展类加载器继续向下通知应用程序加载器加载。这就是JVM的双亲委派机制。
4、为什么要通过双亲委派机制
JVM为什么要通过这种麻烦的方式,来实现类的加载的,原因如下:
- 沙箱机制:JAVA不希望用户或者黑客随便更改核心类库,所以再加载核心类库和扩展包的时候,JAVA要保证加载的是自己核心类库的东西,而不是别人更改过的类。,就是为了安全
- 避免了类的重复加载
5、从代码层次分析双亲委派机制
JAVA的双亲委派的机制的主要概念:
- 引导类加载器由JAVA虚拟机启动以后,调用底层C++代码加载
- 扩展类加载器和应用类加载器由一个被称为启动器的加载,该类就是Launcher, 扩展类加载器和应用类加载器的上下层父子关系由parent属性确立
- Launcher类是由引导类加载器加载的
类的关系图
AppClassLoader和ExtClassLoader都是Launcher(启动器)的内部类,由Launcher调用构造方法的时候,创建AppClassLodader和ExtClassLoder
图一
图二
AppClassLoader和ExtClassLoder都是继承自URLClassLoader
URLClassLoader最终继承自ClassLoader
图三
类的父子关系整理清楚了,下面看Launcher是怎么创建AppcClassLloader和ExtClassLoader的?
可以看到,在创建AppClassLoader时,将创建好的ExtClassLoader传给了AppClassLoader的get**方法。这个方法最终直接会调用到它的构造方法,我们直接看它的构造放的代码。
图四
我们看下图AppClassLoader的构造方法,会调用super,也就是父类的构造方法,根据图三的类的关系图,会最终调用到ClassLoader类的构造方法,我们继续进入到它的构造方法中看它干了什么。
图五
好的我们看到了parent属性,也就是将最初传入的ExtClassLoader的实例设置为APPClassLoader的父亲。
tuliu
图六
总结:引导类加载器加载了启动器Launcher,Launcher创建了AppClassLoader和ExtClasslLoader,并好父子关系(通过parent)
你以为这就完了吗?,不可能,绝对不可能!双亲委派的代码还没说呢,加载类之前向上委托那一系列的操作,在哪能还没说呢,怎么可能就这么结束了呢!!!
我们来看类是怎么加载的,我们都知道,加载一个类的时候我们会调用ClassLoader的loaderClass方法,那我们就看看父子关系建立好以后,是怎么实现向上委托的?
图七
public Class<?> loadClass(String name) throws ClassNotFoundException {
//调用了重载的loadClass方法,多传入一个flase
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//加锁是为了保证加载类class文件时,必须是安全的,不会因为并发加载多个类进来
synchronized (getClassLoadingLock(name)) {
// 先根据类的全限定名查找类是否已经加载过了,加载过了c!=null直接返回。不走下面的代码
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
/**
* 注意:这里就是双亲委派的关键代码
*代码走到这里会让他的parent去递归调用本方法,直到parent==null时(ExtClassload),这里就符合双亲委派机制,向上委托
*/
//AppClassLoader执行这行代码
if (parent != null) {
c = parent.loadClass(name, false);
//扩展类加载器执行这行代码
} else {
//因为ExtClassLoader的partent==null执行这行代码,调用引导类加载器,加载java核心类库的代码返回c==null 代码继续向下执行
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();
//因为之前是递归调用的,第一次是ExtClassLoader调用这个方法,去加载java扩展包的类
//第二次是AppClassLoader调用这个方法,AppCLassLoader是加载classpath路径下我们自己自己写的类,具体实现的方法在URLClassLoader,我们去看URLClassLoader是怎么实现的,如图八
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;
}
}
图八
加载class流文件
图九
总结
- 引导类加载器由底层C++代码创建
- 引导类加载器加载启动器Launcher
- Launcher的构造方法创建ExtClassLoade和AppClassLoader,并通过parent属性确认父子父子关系
- ClassLoader的loadClass方式实现了双亲委派机制
- findClass方法调用defindClass实现classpath下的class文件的加载
图十
三、自定义类加载器
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
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;
}
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();
}
}
}
public static void main(String args[]) throws Exception {
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
//D盘创建 test/com/test/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class clazz = classLoader.loadClass("com.test.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}