JVM-01 类加载过程及源码分析

一、JVM类加载过程及源码分析

我们通过ide写的java代码,毫无疑问是最终需要加载到JVM来运行的。试想JVM作为跨语言的平台,能同时支持多种编程语言(js、groory、scala…等)的字节码文件运行,那么在字节码文件和JVM之间,必须有一套完备的流程,来将字节码文件转化为JVM内存中的变量信息。
下图,是坊间盛传的JVM类加载过程,今天我们来探一探水深。
JVM-01 类加载过程及源码分析_第1张图片

1.1 前言引语

sun.misc.Launcher的方法很简单,静态代码实例了launcher,供getLauncher()方法获取launcher对象。
JVM-01 类加载过程及源码分析_第2张图片
但是,当给我们他通过debug打点到launcher()构造方法获取ExtClassLoader、AppClassLoader时,却发现断点进不到46、52行。是思路错了还是断点无效?

于是我又翻了翻 IBM 关于 Java 中 Debug 实现原理的介绍,文章地址如下:https://www.ibm.com/developerworks/cn/java/j-lo-jpda1/
JDI(Java Debug Interface)是三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。
参考 Oracle 的官方文档:https://docs.oracle.com/javase/9/docs/api/jdk.jdi-summary.html
可以知道 jdi 是一个位于 tools.jar 包下的子包,而 tools.jar 也是由 BootStrap 类加载器负责加载的

可以明确,Debug时调用的是lib/tools.jar的包,而此时启动类加载器势必已经完成启动,所以debug无效。

1.2类加载器

类加载器定义:

  • 启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的类
  • 自定义加载器:负责加载用户自定义路径下的类包
    JVM中默认读取的路径参数:
      class_path = SecuritySupport.getSystemProperty("java.class.path");
      boot_path  = SecuritySupport.getSystemProperty("sun.boot.class.path");
      ext_path   = SecuritySupport.getSystemProperty("java.ext.dirs");

上节了解到,启动类加载器比较神秘,多是由C++来实现的,那我们就先从Luncher着手看看扩展类加载器和应用程序类加载器的结构。

1、首先查看Launcher的结构,内部类有AppClassLoader、ExtClassLoader ,并且这2个内部类都继承了URLClassLoader。下图看URLClassLoader的组织结构,最终继承了ClassLoader类。所以我们需要明确,类加载器之间并没有继承的父子关系,而是ClassLoader中维护了private final ClassLoader parent;字段,用这种方式来标记当前加载器的父加载器。后面在看到双亲委委派机制时,不能搞混了。
JVM-01 类加载过程及源码分析_第3张图片JVM-01 类加载过程及源码分析_第4张图片
2、Luncher使用的ClassLoader ,是由getAppClassLoader()生成的,所以JVM的加载的类,首先都需要从App加载器开始加载。
Luncher的构造方法,生成 ExtClassLoader 、AppClassLoader。

    private ClassLoader loader;
	public ClassLoader getClassLoader() {
        return this.loader;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
        	//创建ExtClassLoader扩展类加载器对象
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        try {
        	//将扩展类加载器作为参数,创建AppClassLoader对象
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
    }

2、AppClassLoader的getAppClassLoader方法,将ExtClassLoader对象 var0传递给超类ClassLoader,并标记ExtClassLoader 扩展类加载器是AppClassLoader应用程序加载器的父加载器。同时ExtClassLoader 在构造时,传入的父加载器参数是null,但实际其父类加载器是启动类加载器,C++实现的所以在java代码中只出现了BootClassPathHolder。

		//var0是前面生成的ExtClassLoader对象
        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    //创建AppClassLoader应用类加载器对象
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
        
	 private ClassLoader(Void unused, ClassLoader parent) {
	 	//可见在AppClassLoader中,设置了parent为ExtClassLoader 
        this.parent = parent;
    }

1.3 类加载过程及双亲委派机制

类加载的过程中,依次向上委托父类加载器进行加载。这就形成了双亲委派机制。结合loadClass(name)方法和下图看类的加载顺序,就不难理解了。
双亲委派机制简单的说,就是app加载器先向上交由父类加载器进行加载,父类中找不到,再由子类加载器自行加载。
为什么要设计成双亲委派机制?

  • 安全机制,防止核心API库被随意篡改。
  • 避免重复加载:当父类已经加载过该类时,子类加载器不再加载。保证被加载类的唯一性。
    核心图一 类加载过程及双亲委派机制
    JVM-01 类加载过程及源码分析_第5张图片
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            //第一步,在已加载的类中查找是否存在
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	//第二步,通过父加载器去加载,如果当前是app加载器,则去ext扩展类加载器中查找,如果是扩展类加载器,则去查启动类加载器
                    if (parent != null) {
                    	//扩展类加载器没有重写loadClass方法,会再次进入本方法
                        c = parent.loadClass(name, false);
                    } else {
                    	//启动类加载器加载某个类
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    //第三步,如果还是没有找到类,那就要去磁盘查找文件,将类加载进来
                    long t1 = System.nanoTime();
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

	//具体实施查找的方法
    protected Class<?> findClass(final String name) throws ClassNotFoundException {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                    	//第四步,将类的全限定名,改写成目录的形式 (将.替换成/)
                        String path = name.replace('.', '/').concat(".class");
                        //第五步,去磁盘目录中查找文件
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                            	///第六步,将文件转化为内存中的类
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

1.4 类初始化过程

从磁盘中读取字节码文件 —> 转为二进制文件 —> 验证 —> 准备 —> 解析 —> 初始化
这其中多数核心代码,仍然是C++实现,Java代码中看不到太多有效代码,无奈略过。
JVM-01 类加载过程及源码分析_第6张图片

    private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:         
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
    	//前期检查
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

1.4 自定义类加载器

  • 预期:
    在加载类的时候,从自定义的路径中去查找文件。所以在实现自定义加载器时,只需要把原加载器中定义加载文件的路径替换为我们自己的路径即可。
  • 思路:
    从上面核心图一中,我们看到最终在查到磁盘中类文件的方法实际是: findClass(name),而扩展类加载器和APP类加载器,实际都没有去实现,而是在父类URLClassLoader中实现的 findClass(name)方法,所以在自定义加载器中,只要重写下方path的路径即可。
    protected Class<?> findClass(final String name) throws ClassNotFoundException {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                    	//查找磁盘文件路径的核心代码
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                            }
                        }
                    }
                }, acc);
        }
        return result;
    }

自定义加载器的功能在1.5中实现。

1.5 打破双亲委派机制

  • 预期:打破双亲委派的目的,就是在加载某个类时,不去父类中完成类的加载,而是自行实现加载的逻辑。
  • 思路:JVM中的加载器在定义加载类时,实际是继承或重写loadClass()来实现的。正如核心图一中的代码逻辑,我们只要不再使用原有的loadClass()逻辑,自行实现自由的加载逻辑即可。
public class MyClassLoader extends ClassLoader {
    //需要加载的自定义加载路径
    private String classPath;

    //指定路径的构造方法
    public MyClassLoader(String path){
        this.classPath = path;
    }

	//修改路径的自定义加载器
    private byte[] loadByte(String name) throws Exception {
    	//保持和原代码大体一致
        String path = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + path + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

    @Override
    protected Class<?> findClass(final String name) throws ClassNotFoundException{
        try {
            byte[] data = loadByte(name);
            //找到文件后,继续沿用原有的加载类的方法
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    @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) {
                long t0 = System.nanoTime();
                try {
                    if(name.startsWith("com.asky")){
                        c = findClass(name);
                    }else {
                        c = this.getParent().loadClass(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/study");
        Class clazz = classLoader.loadClass("com.asky.test");
        Object obj = clazz.newInstance();
        System.out.println(clazz.getClassLoader());
    }
}

在这里插入图片描述

参考文章:https://cloud.tencent.com/developer/article/1590312

你可能感兴趣的:(JVM,jvm)