欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

一篇文章搞定JVM如何加载.class文件

程序员文章站 2024-02-11 21:21:22
...

一、Java虚拟机

JVM是一个内存中的虚拟机,主要运用内存存储,所有类、类型、方法,都是在内存中,这决定着我们的程序运行是否健壮、高效。

二、.class文件加载执行流程图

一篇文章搞定JVM如何加载.class文件

  • Class Loader(类加载器):依据特定格式,加载class文件到内存
  • Execution Engine:(执行引擎)对命令进行解析,解析完毕之后就可以提交到操作系统里面执行了
  • Native Interface:融合不同的开发语言的原生库为Java所用,性能并不如c/c++高,主流的JVM也是由c/c++实现的
  • Runtime Data Area:JVM内存空间结构模型,所写的程序都会被加载到这里

三、一个类从编译到加载再到执行的流程

这里我们用一个例子来进行解释

package com.mtli.reflect;

/**
 * @Description:
 * @Author: Mt.Li
 * @Create: 2020-04-25 13:55
 */
public class Robot {
    private String name;
    public void sayHi(String helloSentence){
        System.out.println(helloSentence + " " + name);
    }

    private String throwHello(String tag){
        return "Hello " + tag;
    }
}
  • 编译器将Robot.java源文件编译为Robot.class字节码文件
  • ClassLoader将字节码转换为JVM中的Class对象
  • JVM利用Class对象实例化为Robot对象

3.1、ClassLoader(类加载器)

  • ClassLoader 在Java中有着非常重要的作用,主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流加载进系统,然后交给Java虚拟机进行连接、初始化等操作。

  • 它的种类如下
    一篇文章搞定JVM如何加载.class文件

  • 还有自定义ClassLoader:Java编写,定制化加载(加载的可以不是class,java,可以是其他类型的)

  • 想要实现自定义ClassLoader,我们需要重写两个关键函数

    • findClass(根据名称或者位置加载.class字节码,然后使用defindClass)和defineClass(生成.class)
  • 可能有人说咋还要生成.class啊,因为findClass引入进来的可能是java语言编译成的.class也有可能是其他语言编译成的,甚至可能是自定义的,我们需要用defineClass将字节数组流解密之后,将该字节流数组生成JVM能够识别的字节码文件。

3.2、自定义CLassLoader

package com.mtli.reflect;

import java.io.*;

/**
 * @Description:自定义ClassLoader
 * @Author: Mt.Li
 * @Create: 2020-04-25 15:37
 */


public class MyClassLoader extends ClassLoader{
    private String path;
    private String classLoaderName;

    public MyClassLoader(String path, String classLoaderName){
        this.path = path;
        this.classLoaderName = classLoaderName;
    }

    // 重写findClass用于寻找类文件
    // defineClass还是用Java自己的
    @Override
    public Class findClass(String name){
        // 以二进制字节流传递进来,然后交给defind
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    // 用于加载类文件
    private byte[] loadClassData(String name) {
        name = path + "/" + name + ".class";
        InputStream in = null;
        ByteArrayOutputStream out = null;
        try{
            in = new FileInputStream(new File(name));
            // 通过ByteArrayOutputStream解密
            out = new ByteArrayOutputStream();
            int i = 0;
            // 没读完继续读
            while((i = in.read()) != -1){
                out.write(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 返回字节码
        return out.toByteArray();
    }
}

我们自定义一个Java类

public class Wali{
	static{
		System.out.println("Hello Wali");
	}
}

用javac对其进行编译生成Wali.class文件

写一个测试类:

package com.mtli.reflect;

/**
 * @Description:
 * @Author: Mt.Li
 * @Create: 2020-04-25 17:08
 */
public class ClassLoaderChecker {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader m = new MyClassLoader("C:/Users/Taogege/Desktop", "myClassLoader");
        Class c = m.loadClass("Wali");
        // 创建实例
        c.newInstance();
    }
}

执行结果:
Hello Wali

3.3、类加载器的双亲委派机制

上面我们在自定义ClassLoader中重写了它的findClass方法,对于实际应用中,我们也有必要了解findClass和loadClass的执行。

jdk1.8中的loadClass()源码:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先我们检查要加载的类是否加载过
            Class<?> c = findLoadedClass(name);
            // c为null,则递归该方法,到当前加载器的父级继续查询
            if (c == null) {
                // native方法
                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
                }
                // 递归回来后,到这里说明根加载器也没有加载过,则开始检测能否
                // 自己寻找并加载class
                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有多种,不同的ClassLoader加载类的方式和路径有所不同,为了实现分工,各自负责各自的区块,使得逻辑更加明确,加载类的时候各司其职,当然存在一个机制,让他们相互协作,形成一个整体,这个机制就是双亲委派机制
一篇文章搞定JVM如何加载.class文件
由上图,我们看到左边是由下而上检测是否曾经加载过这个类,比如先检测用户自定义类加载器
是否加载过这个类,有则提交给JVM使用,节省时间,节省内存空间,没有则继续向它的父级查询,重复步骤,如果没有继续向上直到根加载器,如果还是没有,则会像如图右边,当前的类加载器会自己去加载当前的类,如果不能加载,则向下,看子加载器能否加载,直到最下边,如果还是不能加载,则抛出ClassNotFoundException 异常。这跟我们上边放出的源码流程是差不多的。

为什么要用双亲委派机制去加载类呢

内存空间是宝贵的,我们没有必要保存多份同样的类,所以每次加载类进行查询是否曾经加载过,也就是避免多个同样字节码的加载。

四、类的加载方式

  • 隐式加载:new
    • 这个 方式大家都知道,直接创建类
  • 显式加载:loadClass,forName

4.1、loadClass和forName的区别

  • Class.forName也是可以对类进行加载的,内部实际调用的方法是 Class.forName(className,true,classloader);
    注意它的第二个参数为true,这个参数表示是否进行初始化,默认为true,它会让jvm对指定的类执行加载、连接、初始化操作。
  • ClassLoader.loadClass:从上边的加载分析可以知道,它只负责连接,不会进行初始化等操作,所以static这样的静态代码是不会进行初始化的

文章的最后附上一张类的装载过程便于自己查看:
一篇文章搞定JVM如何加载.class文件

相关标签: Java底层