JVM学习系列:类的加载(加载阶段)

概述

Java中,如果想要运行一段代码,那么就需要先将编写完的.java文件编译成.class文件,再通过加载机制将该.class文件加载到JVM内存中。这里就涉及到了类的加载,一个完整的类加载过程包括:加载、连接(验证、准备、解析)、初始化。需要注意的是,此处的加载仅仅是整个加载流程的第一步,本文也仅仅关注于该步骤。
在加载阶段,主要完成了三件事:

  1. 通过类的全限定类名找打对应的字节码文件
  2. 将字节码文件中定义的静态存储结构转换成方法区的运行时数据
  3. 在堆中生成相应的Class对象,作为访问方法区中这个类的各种数据的入口
    image

类加载器

在Java中,提供了一下三个类加载器:

  • BootstrapClassLoader:引导类加载器,由C++实现,主要用于加载核心类库,包括:jre/lib/resources.jar、jre/lib/rt.jar、jre/lib/sunrsasign.jar、jre/lib/jsse.jar、jre/lib/jce.jar等。
  • ExtClassLoader:扩展类加载器,继承自ClassLoader,主要用于加载jre/lib/ext目录下的类。
  • AppClassLoader:系统类加载器,继承自ClassLoader,主要用于加载当前应用CLASSPATH指定的路径下的类,用户编写的类基本都是通过改加载器进行加载到JVM中的。

如果有兴趣继续深入的话,可以研究一下sun.misc.Launcher这个类,该类主要负责创建ExtClassLoader和AppClassLoader。

双亲委托机制

既然Java提供了3个类加载器,那么这三个加载器之间是怎么协作的呢?JVM中采用的是双亲委托机制。

原理

某个加载器在接收到类加载请求时先查看这个类是否已经加载了,如果没有就将这个加载请求推送至父加载器,由父加载器进行处理。直到最顶层的BoostrapClassLoader,因为再也没有父加载器了,那就由其本身尝试加载,如果无法加载,那就从上到下一次推给子加载器进行加载,如果到最底层的AppClassLoader(此处不考虑用户自定义类加载器)依旧无法加载该类,那么就抛出ClassNotFoundException。
ClassLoader类中的loadClass方法就是双亲委托机制的实现。

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized(this.getClassLoadingLock(name)) {
            //查找该类是否已经被加载
            Class<?> c = this.findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (this.parent != null) {
                        //如果父加载器存在,则交由父加载器加载该类
                        c = this.parent.loadClass(name, false);
                    } else {
                        /*父加载器不存在,则交由BootstrapClassLoader加载。
                        此处的父加载器为null的原因在于ExtClassLoader的父加载类
                        BootstrapClassLoader是由C++编写的,所以虽然ExtClassLoader
                        以其为父加载器,但是返回结果为null
                        */
                        c = this.findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException var10) {
                }
                //如果父加载器没有加载到该类,那就由本加载器负责进行加载
                if (c == null) {
                    long t1 = System.nanoTime();
                    c = this.findClass(name);
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                this.resolveClass(c);
            }
            return c;
        }
    }

优点

双亲委托机制中,委托是从下层加载器到上层加载器,查找路径则是从上层加载器的加载范围到底层加载器的加载范围。
从下到上的委托,避免了出现重复加载一个类的问题,而从上到下的查找路径避免了java核心类被用户自己定义的同名类替换的问题。

自定义加载器

JVM中,除了使用默认提供的3个加载器,还可以自定义类加载器。这种自定义的类加载器有什么作用呢?

  • 可以指定从对应的磁盘路径或网络中加载类
  • 可以对相应的.class文件进行加密,并通过特定解密加载器对其进行加载
  • 从专门的数据库中加载类

那么,如何实现自定义的类加载器呢?可以通过继承ClassLoader类,并重写findClass(String name)方法实现一个自定的类加载器。
以一个从指定位置进行查找的类加载器为例。

被加载类

准备一个待加载的类,将它放到磁盘中的任意位置。

import java.lang.*;

public class Test{
    public void say(){
        System.out.println("hello world");
    }	
}

编写自定义加载器

import java.io.*;

public class DiskClassLoader extends ClassLoader{

    private final String path;

    protected DiskClassLoader(String path) {
        super();
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if(name!=null && !"".equals(name)){
            String classFilePath = getClassFilePath(name);
            File file = new File(classFilePath);
            FileInputStream fileInputStream =null;
            ByteArrayOutputStream byteArrayOutputStream=null;
            try {
                fileInputStream = new FileInputStream(file);
                byteArrayOutputStream = new ByteArrayOutputStream();
                int len = 0;
                try{
                    while((len=fileInputStream.read())!=-1){
                        byteArrayOutputStream.write(len);
                    }
                    byte[] bytes = byteArrayOutputStream.toByteArray();
                    //生成Class对象
                    return defineClass(name,bytes,0,bytes.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } catch (FileNotFoundException e) {
                throw new ClassNotFoundException();
            } finally {
                if(fileInputStream!=null){
                    try {
                        fileInputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if(byteArrayOutputStream!=null){
                    try {
                        byteArrayOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }else{
            throw new ClassNotFoundException();
        }
        return null;

    }

    private String getClassFilePath(String name) {
        int index = name.lastIndexOf(".");
        String classSimpleName = name.substring(index+1);
        return path+(path.endsWith("/")?"":"/")+classSimpleName+".class";

    }
}

加载类并执行方法

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test01 {

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException,
            IllegalAccessException, NoSuchMethodException, InvocationTargetException {

        System.out.println(System.getProperty("java.class.path"));
        //Test.class文件被放到了/home/ttdtc/Desktop
        DiskClassLoader diskClassLoader = new DiskClassLoader("/home/ttdtc/Desktop");
        //此处填写的应该是类的全限定类名,但是类Test没有指定package,所以只填Test
        Class<?> clazz = diskClassLoader.loadClass("Test");
        if(clazz!=null){
            Object obj = clazz.newInstance();
            Method say = clazz.getDeclaredMethod("say");
            say.invoke(obj);
        }else{
            throw new ClassNotFoundException();
        }
    }
}

执行结果

通过自定义的类加载器DiskClassLoader,成功加载了Test类,执行了say方法

结束语

初学JVM,如有出错欢迎纠正,谢谢!

参考

posted @ 2021-12-09 19:41  偷偷地跳槽  阅读(95)  评论(0)    收藏  举报