从JDK源码级别剖析JVM类加载机制
main方法的执行流程
image-20230224180340262
1:先把文件编译成class文件
2:window下,java.exe调用jvm.dll创建虚拟机,dll文件相当于jar包。
3:c++创建引导类加载器。
4:实例化Launcher,由引导类加载器加载。
5:加载JvmTest.class文件
6:执行main方法 loadClass加载流程,类是如何加载到java虚拟机的?static int a = 10; int b = 5;
1:加载:将字节码文件丢到内存
2:验证:需要验证字节码格式是否正确。Cafe baba标准的字节码文件开头。
3:准备:对静态变量进行赋值,赋上默认值,将a赋为0。
4:解析:将符号引用转变成直接引用,只是将静态的能够确定被谁调用的才会被转变,这个叫做静态解析,类加载时候完成。
动态解析,在编译的时候没有办法确定被谁调用,只能在运行的时候才能判断出来,比如多态。类运行时候完成。
符号引用包括三种:类、接口的符号引用。字段的符号引用。方法的符号引用。
直接引用:符号的内存地址。
5:初始话:将静态遍变量真正赋值,将a赋值为10,静态代码块也会在这里执行 。
6:将class文件加载到JVM虚拟机中。 类的加载机制一般是懒加载,在只有用到的时候才会真正的加载。public class JvmTest { static int b = 10; static { System.out.println("jvmTest静态方法"); System.out.println("b的值 = " + b); } public static void main(String[] args) { System.out.println("执行main方法"); new A(); } } class A { static { System.out.println("A的静态方法"); } A() { System.out.println("A的构造方法"); } } class B { static { System.out.println("B的静态方法"); } B(){ System.out.println("B的构造方法"); } }
执行结果 : jvmTest静态方法 b的值 = 10 执行main方法 A的静态方法 A的构造方法
A的静态代码块输出是在main方法之后,也就是在真正使用的时候才会被加载。然后B没有被使用,所以不会被加载。 类加载器和双亲委派机制
类加载器分为:
1:引导类加载器:是由c++创建,在jvm创建之后加载,它负责加载JRE下lib目录的jar。
2:扩展类加载器:负责加载JER下lib的ext,extClassLoader
3:应用程序加载器:负责加载应用程序,appClassLoader
4:自定义加载器:自己定义的 System.out.println("String加载类:" + String.class.getClassLoader()); System.out.println("DESKeyFactory加载类:" + com.sun.crypto.provider.DESCipher.class.getClassLoader()); System.out.println("JvmTest加载类:" + JvmTest.class.getClassLoader());
输出 : String加载类:null DESKeyFactory加载类:sun.misc.Launcher$ExtClassLoader@372f7a8d JvmTest加载类:sun.misc.Launcher$AppClassLoader@18b4aac2
String的加载类是引导类加载器加载的,引导类是由c++生成的,所以看不到,是个null。
extClassLoader是扩展类加载器。
AppClassLoader引用程序加载器。
Launcher
C++引导类加载完之后会实例化Launcher class Launcher{ private ClassLoader loader; private static Launcher launcher = new Launcher(); public static Launcher getLauncher() { return launcher; } ... ... }
launcher是个静态变量,是个单例,在加载的时候就初始话好了。
初始话的时候实例化ext和app ClassLoader public Launcher() { //实例化扩展类加载器,它的父类没有设置值 Launcher.ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader(); //初始化APPclassLoader,并且将ext传入设置为父类加载器 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); }
loader赋值为appClassLoader,默认先用appClassLoader加载 双亲委派机制
image-20230224132150805
C++在加载的时候调用 Launcher.getLauncher().getClassLoader().loadClass("JvmTest.class")
Launcher的classLoader是在实例化的时候赋值了AppClassLoader。
1:AppClassLoader去自己已经加载的类里边找,如果没有向上委托
2:ExtClassLoader去自己已经加载的类里边找,如果没有向上委托。
3:引导类去自己加载的类里边找,如果没有,
4:引导类尝试加载,判断这个类是不是应该由自己加载,判断是不是JRE包路径下的,如果是加载并且返回,不是向下传播
5:ExtClassLoader判断这个类是不是ext包路径下的,如果是自己加载并返回,不是向下传播
6:AppClassLoader判断这个类是不是classPath路径下的,如果是的化加载。不是的话,ClassNotFound
为什么先由AppClassLoader,而不是引导类开始?
因为大部分类都是我们自己写的,百分之95都是存放到AppClassLoader,只有第一次加载的时候会多走一步,之后大部分都直接从AppClassLoader里边获取。
loadClass("JvmTest.class")源码 Class<?> loadClass(String name, boolean resolve){ //从自己加载的类里边找 Class<?> c = findLoadedClass(name); if (c == null) { if (parent != null) { //父加载器加载 c = parent.loadClass(name, false); } else { //ext的父是null,这是最后一层,引导类类加载器 c = findBootstrapClassOrNull(name); } if (c == null) { //由URLClassLoader实现 c = findClass(name); } } return c; } URLClassLoader.findClass源码Class<?> findClass(final String name){ String path = name.replace(".", "/").concat(".class"); //判断是不是自己要加载的。jrt/ ext/ Resource res = ucp.getResource(path, false); if (res != null) { //真正加载class的方法 return defineClass(name, res); } if (result == null) { throw new ClassNotFoundException(name); } return result; }
新增一个String类 package java.lang; public class String { public static void main(String[] args) { System.out.println(111); } }
执行报错信息: 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application
我们新增的 java.lang.String,是能够在引导类加载器的JRE包下边找到的,并且返回String类,这个返回的是jdk自带的类,并不是我们自己写的类,所以会报找不到main方法。
jdk为什么要用双亲委派机制 ?
jdk不让修改自己内部的类,沙箱安全机制,防止核心API被篡改。
避免类的重复加载,父加载器已经加载完了,自己就不用再加载了。
全盘负责 : public class JvmTest { static User user; }
当加载JvmTest类的时候,也会加载User类,这两个类都会由同一个类加载器加载,不会由乱七八糟的加载器加载。 自定义加载器
我们要自定义加载器,只需要实现ClassLoader,重写findClass就可以。 public 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; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); //真正的加载步骤 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } public static void main(String[] args) throws Exception { MyClassLoader myClassLoader = new MyClassLoader("C:/myClassLoader"); Class<?> clazz = myClassLoader.loadClass("com.bbk.code.User"); Object o = clazz.newInstance(); Method method = clazz.getDeclaredMethod("eat"); method.invoke(o); System.out.println("当前MyClassLoader的类加载器:"+MyClassLoader.class.getClassLoader()); } }
自定义classLoade继承ClassLoader初始话会先初始化父类,在这时候会给自定义classLoader赋值parent为AppClassLoader。具体代码体现在: protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; ... ... } public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); ... ... return scl; } private static synchronized void initSystemClassLoader() { ... ... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); scl = l.getClassLoader(); //返回this.loader,引导类在加载Launcher的时候会赋值为AppClassLoader ... ... }
执行main方法,需要把User的class从target中删除,并且加入到我们自定义的文件夹中。执行输出 我是User类eat方法,我的加载器是com.bbk.code.MyClassLoader@5a07e868 如何打破双亲委派机制
意思就是不在委托父加载,直接自己加载类。双亲委派的逻辑代码实现在loadClass方法中。需要继承ClassLoader重写loadClass方法即可:
image-20230224173458980
把红框的代码删除掉。 @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) { c = findClass(name); } if (resolve) { resolveClass(c); } return c; } }
重新执行main方法,报错: java.io.FileNotFoundException: C:myClassLoaderjavalangObject.class (系统找不到指定的路径。)
在加载User的时候,会现在父类Object,我们打破双亲委派之后,自定义的加载器不存在Object所以会报错。
我们把Object.class放入到我们的本地文件夹中。重新执行报错: //禁止加载java.lang 包 java.lang.SecurityException: Prohibited package name: java.lang
java.lang必须由我们引导类加载器加载,沙箱安全。我们只能让我们的引导类去加载java.lang。修改代码 @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) { if(!name.startsWith("com.bbk")){ c = this.getParent().loadClass(name); }else{ c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }
只有当name是由com.bbk开头的由我们加载器加载。其他的都由父加载器加载。不会报错。