从JDK源码查看类加载过程
Java代码执行过程
执行Java代码的过程:
Java肯定是没办法对系统资源进行初始化的,所以首先肯定是由c++来初始化资源,例如创建Java虚拟机等,执行Java.exe调用JVM.dll创建Java虚拟机 -> 调用由c++编写的引导类加载器,该类加载器会加载核心类库 -> 调用sun.misc.Launcher的构造方法会创建Launcher类对象,在该构造方法中会创建扩展类加载器和应用类加载器
执行过程如下图所示
因为Java类加载模式是懒加载,所以并不会在一开始就将所用类一次性加载到JVM中,会根据双亲委派机制对类进行加载。
各种类加载器
引导类加载器: 用于加载核心类库,比如rt.jar、或String.class,引导类加载器为最上层加载器,加载的目录为\JDK\1.8\jre\lib
扩展类加载器:用于加载扩展类包,扩展类加载器在引导类加载器下层,加载目录为JDK\jre\lib\ext
应用类加载器:用于加载用户自定义的类,加载目录为target目录下的class文件。
自定义类加载器: 用户自定义类加载器,加载目录可以自定义。
类加载的过程
加载:将class文件以二进制数组读取到内存中。
验证:校验class文件的字节码是否符合JVM规则。
初始化:给静态变量分配内存并设置默认值。
解析:将符号引用转换为直接引用,该阶段会将静态符号引用转换为内存地址值用于执行,这个过程被称为静态链接,动态链接是指在程序执行过程中进行符号引用转换。
初始化:为静态变量赋值并执行静态语句块。
符号引用:硬盘中保存的class文件中的main class 类名之类的,一般这些信息存储在静态常量池中,使用javap命令可以看到class中的符号引用有哪些,这系符号引用会在解析阶段替换为内存地址。
静态常量池是存储在.class类文件中。在执行编译Java代码的过程中,编译器并不知道方法名、变量名等一些字段会被加载到内存的什么地方,只能先将其替换成符号,在需要执行class方法的时候才会将其加载到内存中并替换为直接引用,直接引用就是内存引用。
双亲委派机制
当进行类加载时,会将类先委托到最上层类进行加载,如果上层已经加载过了那么直接返回,如果没有被加载过,那么就会校验当前类加载器是否与被加载的类匹配,如果匹配则进行加载,如果不匹配则向下传递,由下层类进行加载。
这是JDK源码的类加载相关
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 (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;
}
}
当类进行加载时会在此检查类是否被加载过 如果被加载过则直接进行返回
之后会委托上层类进加载器进行加载,上层类加载器依旧会调用loadClass判断是否类是否被加载过 如果被加载过则直接进行返回。如果parent为空的话那么就表示已经到引导类加载器了,然后使用引导类加载器进行加载。
之后就会执行当前类加载器的加载功能,在改方法中还会判断是否为合法的加载路径。
如果不能加载成功,因为是嵌套掉的的,所以会返回下一层类加载器进行加载,这样就做到了双亲委派机制。
自定义类加载器
在AppClassLoader中有个findClass方法用于加载类,我们只要模仿这个类便可以自定义一个类加载器出来,该类的主要功能是将传入的class转为byte数组,然后调用defineClass进行加载,defineClass方法中的调用有许多本地方法,没办法查看是如何实现类加载的,调用该方法经过了以上介绍的类加载的过程。
该类的loadBytes将传入的文件以字节流的格式进行读取并保存到字节数组中返回。
findClass方法获取字节数组并调用defineClass进行加载。
public class LyraClassLoader extends ClassLoader {
private String path;
public LyraClassLoader(String path) {
this.path = path;
}
public byte[] loadBytes(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(path + "/" + name + ".class");
int available = fileInputStream.available();
byte[] bytes = new byte[available];
fileInputStream.read(bytes);
fileInputStream.close();
return bytes;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes;
try {
bytes = this.loadBytes(name);
} catch (IOException e) {
throw new RuntimeException(e);
}
return defineClass(name, bytes, 0, bytes.length);
}
}
打破双亲委派机制
双亲委派机制是由loadClass方法实现的,我们将这个类重写一下,然后复制原来的代码 将双亲委派机制相关的代码删掉就行了
但是代码还是有些问题,因为打破了双亲委派机制,无法委托上层加载Object类,所以会报错。
解决方法是判断传入的包名是否为自定义的类,如果不是的话就用父类的类加载器进行加载,这样就可以非自定义包委托上层加载了。
自定义类加载器的parent默认为应用类加载器,因为
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律