jvm系列:类加载机制

1,469 阅读7分钟

这是我参与 8 月更文挑战的第 3 天,活动详情查看: 8月更文挑战

类加载器

    类加载器(ClassLoader):其作用是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后转换为一个与目标类对应的java.lang.Class对象实例。JVM规范允许类加载器在某个类将要被使用时可以预先加载它。

类加载机制类加载的流程

java程序运行时,实际上是JDK在执行了java命令,指定了包含main方法的完整类名,以及一个classpath类路径,作为程序的入口,然后根据类的完全限定名查找并且加载类,而查找的规则是在系统类和指定的文件类路径中寻找。如果是class文件的根目录中,则直接查看是否有对应的子目录以及class文件,如果当前路径是jar文件,首先执行解压,然后再去到目录中查找是否有对应的类。而这个查找加载的过程中,负责完成操作的类就是ClassLoader类加载器,输入为完全限定类名,输出是对应的Class对象,类加载器有如下几种:

BootstrapClassLoader:是Java虚拟机内部实现的,此类负责加载java的基础类即“JAVA_HOME/lib”目录中的所有类型,或者由“-Xbootclasspath”指定路径中的所有类型。如String、Array等class,还有jdk文件夹中lib文件夹目录中的rt.jar

ExtClassLoader:继承自ClassLoader抽象类,默认的实现类是sun.misc.Launcher包中的ExtClassLoader类,此类默认负责加载JDK中一些扩展的jar,如lib文件夹中ext目录中的jar文件

ExtClassLoader:负责加载“JAVA_HOME/lib/ext”目录下的所有类型。

AppClassLoader:ClassLoader类加载器的默认实现类为sun.misc.Launcher包中的AppClassLoader类,此加载器默认负责加载应用程序的类,包括自己实现的类与引入的第三方类库,即会加载整个java程序目录中的所有指定的类。

类加载过程

一个完整的类加载过程必须经历加载、连接、初始化这三个步骤

流程.PNG

加载

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为既可以使用系统提供的类加载器来完成加载,也可以自定义类加载器来完成加载。加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

验证

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。 确保二进制字节流格式符合预期,是否所有方法都遵守访问控制关键字的限定,方法调用的参数个数和类型是否正确,确保变量在使用之前被正确初始化了,检查变量是否被赋予恰当类型的值。

准备

JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。

也就是说,假如有这样一段代码:

public String test1 = "掘金1";
public static String test2 = "掘金2";
public static final String test3 = "掘金3";
test1 不会被分配内存,而 test2 会;但 test2 的初始值不是“王二”而是 null

需要注意的是,static final 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 test3 在准备阶段的值为“掘金3”而不是 null。

解析

该阶段将常量池中的符号引用转化为直接引用。

符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。直接引用通过对符号引用进行解析,找到引用的实际内存地址。

初始化

该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。

举个例子

String juejin = new String("掘金");

上面这段代码使用了 new 关键字来实例化一个字符串对象,那么这时候,就会调用String类的构造方法对juejin进行实例化。

双亲委派模型

如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器, 一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。 使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。

自定义类加载.PNG

其意义就是如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。这样就防止了内存中出现多份同样的字节码,保证Java程序安全稳定运行。

自定义类加载器

    

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;
    }
}

这个是ClassLoader中的loadClass方法,大致流程如下: 1)检查类是否已加载,如果是则不用再重新加载了;

2)如果未加载,则通过父类加载(依次递归)或者启动类加载器(bootstrap)加载;

3)如果还未找到,则调用本加载器的findClass方法;

类加载器先通过父类加载,父类未找到时,才有本加载器加载。在loadClass方法执行过程中,还会传递一个resolve标示符,此标示符和forName的initialize参数是一样的,用来判断是否在Class加载后进行初始化代码块的操作,但是我们从上面的方法明显看到,默认传递的值为false,即仅仅加载Class类,并不去调用类的初始化代码块部分。测试代码如下:

加载的类

public class Juejin {

    public Juejin() {
        System.out.println("Juejin:" + getClass().getClassLoader());
        System.out.println("Juejin Parent:" + getClass().getClassLoader().getParent());
    }

    public String print() {
        System.out.println("Juejin:print()");
        return "JuejinPrint";
    }
 }

TClassLoader:

class TClassLoader extends ClassLoader {

private String classPath;

public TClassLoader(String classPath) {
    this.classPath = classPath;
}

@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();
    }

}

//获取.class的字节流
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();

    // 字节流解密
    data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);

    return data;
  }
}
@Test
public void testClassLoader() throws Exception {
    TClassLoader myClassLoader = new HClassLoader("D:/demo/a");
    Class clazz = myClassLoader.loadClass("com.Demo.Juejin");
    Object o = clazz.newInstance();
    Method print = clazz.getDeclaredMethod("print", null);
    print.invoke(o, null);
}