一. Java类加载运行全过程
以Math类为例子,代码如下:
public class Math {
public static int compute(){
int a = 1;
int b = 3;
return (a + b) * 10;
}
public static void main(String[] args) {
System.out.println(Math.compute());
}
}
当运行Math类中的main方法时,程序是如何运行的,流程大致如下:
- 参见类运行加载全过程图可知其中会创建JVM启动器实例
sun.misc.Launcher
。
Launcher
初始化使用了单例模式
设计,保证一个JVM虚拟机内只有一个Launcher
实例。
在Launcher
构造方法内部,其创建了两个类加载器
,分别是ExtClassLoader(扩展类加载器)
和AppClassLoader(应用程序类加载器)
JVM默认通过使用Launcher的getAppClassLoader()
方法返回的AppClassLoader
类加载器来加载我们的应用程序。
package sun.misc;
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
//C++ 调用Java代码创建JVM启动器,并且实例化Launcher
public static Launcher getLauncher() {
return launcher;
}
//Launcher 类的构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//构建扩展类加载器ExtClassLoader,在构建的过程中将其父类加载器(引导类加载器)设置为null
//所以输出引导类加载器为null,因为引导类加载器由C++实现
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构建应用程序类加载器AppClassLoader,在构造的过程中,将其父类加载器设置为扩展类加载器ExtClassLoader
//Launcher的loader变量值为AppClassLoader实例,一般都是用这个类加载器来加载自己编写的应用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
............此处源码省略
}
- 类加载器全部创建完成,并且各自已经加载完各自的目录文件,此时JVM的
运行环境
才算初步建立完成,获取对应的类加载器(AppClassLoader)去加载Math.class字节码文件
,大致流程如下:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
加载环节分析:
- 加载:在硬盘上查找并通过
IO
读入字节码文件
,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区
这个类的各种数据的访问入口
- 验证:校验字节码文件的正确性
- 准备:给类的
静态变量
分配内存,并赋予默认值
- 解析:将
符号引用
替换为直接引用
,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针
或句柄
等(直接引用),这是所谓的静态链接
过程(类加载期间完成),动态链接
是在程序运行期间完成的将符号引用替换为直接引用 - 初始化:对类的静态变量初始化为指定的值,执行
静态代码块
查询字节码文件命令: javap -v User1.class 一定要进入class文件的目录下
类被加载到方法区
中后主要包含运行时常量池
、类型信息
、字段信息
、方法信息
、类加载器的引用
、对应class实例的引用
等信息。
名词 | 说明 |
---|---|
运行时常量池 | 是Java虚拟机在运行时创建的一种用于存放字面量和符号引用的表。它是各种指令操作数、类和接口中常量池的运行时表示形式。常量池中存储了各种类型的常量,如字符串、数字、类和接口的符号引用等 |
类型信息 | 是类中声明的变量的类型信息,包括变量名、访问修饰符、类型等,用于在编译期和运行时确定变量的属性和行为 |
字段信息 | 是类中声明的变量的信息,包括变量名、访问修饰符、类型等,以及对应的字段值、字段注解等信息,用于在编译期和运行时确定变量的属性和行为 |
方法信息 | 是类中声明的方法的信息,包括方法名、返回值类型、参数类型等,以及对应的方法体、方法注解等信息,用于在编译期和运行时确定方法的属性和行为 |
类加载器的引用 | 是加载该类的类加载器对象的引用。每个类都有一个对应的类加载器,它负责将类的字节码文件加载到内存中,并生成对应的Class实例 |
对应class实例的引用 | Class实例是Java中表示类的对象,它包含了类的所有信息,包括类型信息、字段信息、方法信息等。对应Class实例的引用指的是程序中持有该Class实例的引用。可以通过该引用来获取类的信息和操作类的属性和行为 |
二. 从JDK源码级别剖析JVM核心类加载器
JVM的类加载器类型有哪些?
- 引导类加载器 (BootstrapLoader)
负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
- 扩展类加载器(ExtClassLoader)
负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
- 应用类程序加载器(AppClassLoader)
负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
通过一段代码验证下,三个加载器加载的路径:
public class JDKClassLoaderTest {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader()); //JDK核心类,是由引导类加载器加载,由于此加载器是由底层C++实现的,所以此处无法输出,显示为null
System.out.println(DESKeyFactory.class.getClassLoader()); //JDK的ext目录下的类,由扩展类加载器加载
System.out.println(JDKClassLoaderTest.class.getClassLoader()); //是本地生成的classpath路径下的class文件,由应用程序类加载器加载
System.out.println();
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassLoader.getParent();
System.out.println("应用程序类加载器=" + appClassLoader);
System.out.println("应用程序类加载器的父级类加载器是=" + extClassLoader);
System.out.println("扩展类加载器的父级类加载器是=" + bootstrapLoader);
System.out.println();
System.out.println("bootstrapLoader加载一下文件:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); //引导类加载器加载加载的 /jdk1.8.0_261/jre/lib 目录下的jar包
for (URL urL : urLs) {
System.out.println(urL);
}
System.out.println();
System.out.println("extClassLoader加载一下文件:");
System.out.println(System.getProperty("java.ext.dirs")); //扩展类加载器加载的是 /jdk1.8.0_261/jre/lib/ext 目录下的jar包
System.out.println("appClassLoader加载一下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
运行结果
从运行的结果可以看出,应用程序类加载器
的加载目录包含了引导类加载器
和扩展类加载器
的加载目录
虽然打印出了目录,但是应用程序类加载器会将target/class
目录下的文件加载到指定内存中,其他目录的文件是不会加载到指定内存中的。
为什么会这样? 双亲委派机制
三. 从JDK源码级别剖析类加载双亲委派机制
双亲委派机制
是指在一个层次结构中的类加载器,在加载类时,会优先把请求交给父类加载器处理,直到最顶层的类加载器,如果父类加载器无法加载该类,再由子类加载器来尝试加载。
- 双亲委派机制的流程
源码ClassLoader.loadClass(),诠释了双亲委派机制的过程
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); //检查当前类加载器是否已经加载了该类 name = com.maker.jvm.Math
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) {
resolveClass(c);
}
return c;
}
}
大致过程如下:
- 加载Math.class时,
应用程序类加载器
(AppClassLoader)最先接收到类加载的请求,然后在自己的专属内存区域中
检查是否已经加载过了,如果加载了,直接返回使用,如果没有加载,向上委托父级扩展类加载器
(ExtClassLoader)去加载。 - 扩展类加载器先查看自己专属内存区域中是否已经加载过该类,如果加载了,直接返回,没有加载,扩展类加载器会向上委托父加载器,也就是引导类加载器去处理。
- 引导类加载器查看自己专属内存区域中是否已经加载过该类,如果加载了,直接返回,没有加载,会尝试自己加载。
- 由于Math.class在classpath路径下,并不在引导类加载器的加载范围,引导类加载器会加载失败,向下委托子加载器扩展类加载器去加载,扩展类加载器加载失败,会向下委托子加载器应用程序类加载器加载。
- 应用程序类加载器先查看自己专属内存区域中是否已经加载过该类,如果加载了,直接返回,没有加载,则加载类,并且缓存在自己的专属区域中。
总结: 双亲委派 ,双向委托
2. 为什么要设计双亲委派机制?
① 沙箱安全机制: 自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
② 避免类的重复加载: 当父级类加载器已经加载了该类时,就没有必要子类加载器再加载一次,保证被加载类的唯一性
package java.lang; //模拟JDK核心类String的路径,java.lang.String
public class String {
public static void main(String[] args) {
//JDK 核心类库中的String类,包路径是在java.lang.String
//模拟此路径,验证本地的String是否会被加载
System.out.println("使用的类加载器类型=" + String.class.getClassLoader());
}
}
由于双亲委派,String类在引导类加载器中被找到,而引导类加载器中加载的String类无main方法,所以报以上错误。
引导类加载器和扩展类加载器中已经加载了类,就不需要重复加载了
3. 全盘负责委托机制
当一个类加载器加载一个类时,除非指定其他类加载器,那么该类中依赖或者引用的其他类也都由当前这个类加载器加载
public static void main(String[] args) {
System.out.println("加载Math的类加载器:" + Math.class.getClassLoader());
System.out.println("加载User的类加载器:" + User.class.getClassLoader());
System.out.println("加载Integer的类加载器:" + Integer.class.getClassLoader());
}
四. 自定义类加载器打破双亲委派机制
类加载器采用双亲委派机制
,即优先将类加载请求传递给父类加载器,如果父类加载器无法加载,则由当前类加载器进行加载。这种机制可以保证类的安全性和可靠性,但有时需要通过自定义类加载器打破双亲委派机制,以实现一些特殊的需求。
要实现自定义类加载器打破双亲委派机制,需要创建一个继承自ClassLoader
的自定义类加载器,并重写findClass()
方法,该方法可以根据自定义的规则进行类的查找
和加载
。在加载过程中,可以使用defineClass()
方法将字节数组转换为Class对象,以实现类的加载。
以下是一个简单的例子,演示如何通过自定义类加载器打破双亲委派机制
public class User {
public void sayHello() {
System.out.println("Hello, World! User是由" + User.class.getClassLoader() + "加载的");
}
}
public class MyClassLoader extends ClassLoader {
/**
* 定义一个类加载路径
*/
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//从指定路径加载类
byte[] data = loadClassData(name);
if (data != null) {
// 定义类
return defineClass(name, data, 0, data.length);
}
throw new ClassNotFoundException();
}
private byte[] loadClassData(String name) {
// 从指定路径加载类数据
String fileName = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
File file = new File(fileName);
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
fis = new FileInputStream(file);
bos = new ByteArrayOutputStream();
int len = 0;
byte[] buf = new byte[1024];
while ((len = fis.read(buf)) != -1) {
bos.write(buf, 0, len);
}
return bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D:\\test");
System.out.println("自定义类加载器的父级类加载器=" + myClassLoader.getParent());
Class<?> clazz = myClassLoader.loadClass("com.maker.jvm.User");
Object obj = clazz.newInstance();
Method method = clazz.getMethod("sayHello");
method.invoke(obj);
}
}
运行结果:
在这个示例代码中,我们自定义了一个类加载器MyClassLoader,它继承
自ClassLoader类,并实现了findClass()
方法。在findClass()
方法中,我们从指定路径加载类,并用defineClass()
方法定义类。
这样,我们就可以通过自己的类加载器加载指定路径中的类。同时,我们还可以通过重载
loadClass()方法打破双亲委派机制,从而实现自己的加载策略。
JVM系列博文:
【JVM】第一篇 从JDK源码级别彻底剖析JVM类加载机制
【JVM】第二篇 JVM内存模型深度剖析与优化
【JVM】第三篇 JVM对象创建与内存分配机制深度剖析
【JVM】第四篇 垃圾收集器ParNew&CMS底层三色标记算法详解
【JVM】第五篇 垃圾收集器G1和ZGC详解