类的初始化流程
启动JVM来分析类加载流程
首先我们写一个简单的java程序
package com.jon;
import sun.security.pkcs11.P11Util;
/**
* @author suym
* @createTime 2021/7/23
* @description
*/
public class StudyClassLoader{
public static void main(String[] args) {
System.out.println("Hello,World!");
ClassLoader loader = P11Util.class.getClassLoader();
System.out.println(loader);
}
}
启动程序
可以在idea的vm options 添加 -verbose:class 来打印类加载的过程。
程序的运行过程
windows开始启动java.exe程序,java.exe 程序将完成以下步骤:
- 根据JVM内存配置要求,为JVM申请特定大小的内存空间,这个内存将会成为程序的运行内存(可以通过jdk/bin中的jvisualvm.exe查看内存空间);
- 创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;
- 创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader;
- 使用上述获取的ClassLoader实例加载我们定义的 com.jon.StudyClassLoader类;
- 加载完成时候JVM会执行StudyClassLoader类的main方法入口,执行StudyClassLoader类的main方法;
- 结束,java程序运行结束,JVM销毁。
JVM的运行内存分配
加载系统类
创建一个引导类加载器实例,初步加载系统类到内存方法区区域中
即{JRE_HOME}/lib下面的lib文件,此时JVM的内存空间如下图所示:
- 引导类加载器(Bootstrap Classloader)将系统类加载到运行内存后,对于某些特殊的类,方法区应该有运行时常量池,类型信息,字段信息,方法信息,类加载器引用,Class实例引用等
- 引导类加载器由于是通过c++语言实现,所以这些类的类加载器引用为null
- 对应Class实例引用,类加载器在加载类信息放在方法区后,会在heap中创建一个Class实例,作为开发人员访问方法区中引用类信息的入口
创建JVM启动类实例Launcher,并取得类加载器
当前jvm运行环境准备就绪,jvm将要调用sun.misc.Launcher的静态方法getLauncher(),获取Launcher实例;
//获取Java启动器
sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher();
//获取类加载器ClassLoader用来加载class到内存来
ClassLoader classLoader = launcher.getClassLoader();
为了保证JVM只有一个Launcher实例,在Launcher内部使用了单例模式;在Launcher中还有两个类加载器,这两个类加载器分别是拓展类加载器(Extension ClassLoader) 和 应用类加载器(Application ClassLoader);
扩展类加载器用来加载{JRE_HOME}/lib/ext的jar文件,应用类加载器用来加载自定义的类
除了引导类加载器(Bootstrap Class Loader )的所有类加载器,都有一个能力,就是判断某一个类是否被引导类加载器加载过,如果加载过,可以直接返回对应的Class instance,如果没有,则返回null。图上的指向引导类加载器的虚线表示类加载器的这个有限的访问 引导类加载器的功能。
此时的 launcher.getClassLoader() 方法将会返回 AppClassLoader 实例,AppClassLoader将ExtClassLoader作为自己的父加载器
上面讨论的应用类加载器AppClassLoader的加载类的模式就是我们常说的双亲委派模型(parent-delegation model).
对于某个特定的类加载器而言,应该为其指定一个父类加载器,当用其进行加载类的时候:
- 委托父类加载器帮忙加载;
- 父类加载器加载不了,则查询引导类加载器有没有加载过该类;
- 如果引导类加载器没有加载过该类,则当前的类加载器应该自己加载该类;
- 若加载成功,返回 对应的Class 对象;若失败,抛出异常“ClassNotFoundException”。
请注意:
双亲委派模型中的"双亲"并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器。上面的步骤中,有两个角色:
- 父类加载器(parent classloader):它可以替子加载器尝试加载类
- 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。
类加载器加载main方法
通过 launcher.getClassLoader()方法返回AppClassLoader实例,接着就是AppClassLoader加载com.jon.StudyClassLoader类的时候了。
ClassLoader classloader = launcher.getClassLoader();//取得AppClassLoader类
classLoader.loadClass(“com.jon.StudyClassLoader”);//加载自定义类
上述定义的ocom.jon.StudyClassLoader类被编译成com.jon.StudyClassLoader class二进制文件,这个class文件中有一个叫常量池(Constant Pool)的结构体来存储该class的常量信息。
当AppClassLoader要加载 com.jon.StudyClassLoader类时,会去查看该类的定义,发现它内部声明使用了其它的类: sun.security.pkcs11.P11Util、java.lang.Object、java.lang.System、java.io.PrintStream、java.lang.Class;org.luanlouis.jvm.load.Main类要想正常工作,首先要能够保证这些其内部声明的类加载成功。所以AppClassLoader要先将这些类加载到内存中。(注:为了理解方便,这里没有考虑懒加载的情况,事实上的JVM加载类过程比这复杂的多)
类的加载顺序
- 加载java.lang.Object、java.lang.System、java.io.PrintStream、java,lang.Class
AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现不是其加载范围,其返回null;AppClassLoader发现父类加载器ExtClassLoader无法加载,则会查询这些类是否已经被BootstrapClassLoader加载过,结果表明这些类已经被BootstrapClassLoader加载过,则无需重复加载,直接返回对应的Class实例; - 加载sun.security.pkcs11.P11Util
此在{JRE_HOME}/lib/ext/sunpkcs11.jar包内,属于ExtClassLoader负责加载的范畴。AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现其正好属于加载范围,故ExtClassLoader负责将其加载到内存中。ExtClassLoader在加载sun.security.pkcs11.P11Util时也分析这个类内都使用了哪些类,并将这些类先加载内存后,才开始加载sun.security.pkcs11.P11Util,加载成功后直接返回对应的Class<sun.security.pkcs11.P11Util>实例; - 加载com.jon.StudyClassLoader
AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现不是其加载范围,其返回null;AppClassLoader发现父类加载器ExtClassLoader无法加载,则会查询这些类是否已经被BootstrapClassLoader加载过。而结果表明BootstrapClassLoader 没有加载过它,这时候AppClassLoader只能自己动手负责将其加载到内存中,然后返回对应的Class<com.jon.StudyClassLoader>实例引用。
如上图所示:
JVM方法区的类信息区是按照类加载器进行划分的,每个类加载器会维护自己加载类信息;
某个类加载器在加载相应的类时,会相应地在JVM内存堆(Heap)中创建一个对应的Class,用来表示访问该类信息的入口。
使用StudyClassLoader类的main方法作为程序入口运行程序
调用StudyClassLoader类的main方法作为程序入口,开始执行程序。
方法执行完毕,JVM销毁,释放内存
方法执行完成后,jvm销毁,释放内存。
总结
类加载器
类加载器,顾名思义就是加载类的工具,当前jvm实现的主要有三种类加载器:引导类加载器(Bootstrap Class Loader)、拓展类加载器(Extension Class Loader )、应用加载器(Application Class Loader),我们也可以自己去自定义我们自己的类加载器
- 引导类加载器:该类主要有c++实现,用来加载jvm运行时所需要的lib下的系统类,由于是c语言实现,无法被java代码访问,但是我们可以查询类是否被引导类加载过。
- 拓展类加载器:由java实现,主要用来加载lib/ext下的拓展类,该类加载器是jvm中可以访问到的顶级父类加载器,它没有其他父类加载器。
- 应用类加载器:应用类加载器主要作为加载用户程序的类,会将拓展类加载器当成自己的父类,使用双亲委托机制加载类
- 自定义类加载器:继承java.lang.ClassLoader
双亲委托机制
//提供class类的二进制名称表示,加载对应class,加载成功,则返回表示该类对应的Class<T> instance 实例
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经被当前的类加载器记载过了,如果已经被加载,直接返回对应的Class<T>实例
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();
// 自己尝试加载
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;
}
}
流程图如下
双亲委托机制
- 引导类加载器先加载完系统类,然后获取Lanucher实例,使用应用类加载器进行加载类;
- 应用类加载器加载类文件,如果已经加载直接返回,如果没有加载,判断是否由父类加载器,如果有,让父类加载器先去加载(父类加载器会先判断是否已经加载过这个类文件,如果已经加载直接返回,如果没有加载,继续判断是否有父类加载器)以此保证ext的类都是由拓展类加载器进行加载。