美文网首页
白话类加载

白话类加载

作者: 瑞瑞余之 | 来源:发表于2019-11-21 16:19 被阅读0次

    无论你看哪个版本JVM书籍,类加载是绕不开的开篇第一课,然而我们对其理解往往受限于JVM繁复的概念,而无法真正消化,本文力求图文结合,用大白话让读者真正理解JVM类加载阶段。
    JVM类加载的阶段可以分为:加载、连接、初始化。标准的类加载阶段其实包括了5个步骤,其中连接又分为验证、准备、解析。


    类加载阶段

    我们一步步来理解:当我们的Java源文件(.java)文件被编译器编译成.class的字节码文件后,这个字节码文件还是存在于我们的硬盘上,我们可以在项目结构中看到这些编译后的class文件:


    字节码文件

    加载

    java虚拟机要操作它,第一步当然是将硬盘上的字节码文件放到内存当中,这个过程就是类加载过程的第一步加载(在刚学习JVM的时候,往往把类加载过程和这个加载阶段混淆,客观来说,我也觉得这两个加载取名不够好)。这里有几点需要明确!

    • .class文件加载到内存中什么区域?
      系统内存会分配给JVM一块专门的工作内存,我们称之为运行时数据区,这个区域对于JVM的学习来说尤其重要,这里先针对问题简单介绍,后面会详细解释各部分的功能,在运行时数据区中有一块区域称之为方法区或者元空间,经过第一步加载,类的数据就进入了这个内存部分:

      类加载进入方法区
    • 怎么理解.class文件加载到内存当中?
      通过图示我们可以看到,方法区中存放的是class A的相关信息,具体包括:常量、静态变量、方法信息等等。这里我们再进一步:


      方法区中存放的类信息

      当类被加载到方法区之后,可以看到,一个类被解构到两个部分,可以看出,除了类中声明的常量进入了方法区中的常量池以外,class的其他信息以class标记,存放在方法区中。这就说清楚了一个问题,当我们在new一个对象的时候,JVM怎么知道这个对象长什么模样?答案就在这里,因为在方法区记录着这个类的所有基本信息!

    • 除此之外还有什么结果产生
      其实类在加载这个阶段,除了将类数据载入到方法区,并转化成方法区运行时数据结构以外,还有一个重要产物:在堆内存中生产一个代表这个类的java.lang.Class对象(注意这个对象是类的对象,而不是类的实例对象),作为方法区这类各种数据的访问入口。怎么理解这句话呢:如果你了解反射机制,应该知道我们可以通过反射的方式,根据类的定义创建对象实例,那么问题来了,我们通过Class.forName(全域名)这种方式如何获取到一个类的基本呢信息呢,前面我们说了,类的基本信息存放在方法区中,对于new一个对象实例,类的数据信息是从这里来的。而反射获取类信息的方式就是通过在加载阶段,堆内存中生成的java.lang.Class对象来进行获取。

    说到这里我们了解了JVM会将编译好的字节码文件加载到虚拟机允许时环境的方法区当中,这些信息会从字节码文件格式转换成方法区运行时数据格式,最重要的是JVM会根据这个类的信息在堆内存当中创建一个类对象

    到此为止加载的结果已经清楚了。但还有两个问题是需要解决的:1.类加载的时机,2.类加载的过程。我们先来看第一个问题:

    1. 类加载的时机:

    Java虚拟机规范对类加载的时机没有明确规定,但肯定是发生在运行时当中。看一些文章会将类的加载时机和类的初始化时机混淆,这里强调一下,类的加载最终目的是将class文件加载到方法区并在堆中创建对应的class对象。而类的初始化是给静态变量赋上正确的值,它发生在加载、连接之后,所以不是一回事。类的加载与类的初始化不同,它不需要等到该类被首次主动使用(后面会解释)时才去加载,JVM运行类加载器在预料到某个类要被使用时就提前加载它。这个时候如果加载出现了错误,JVM不会立即报错,而是在程序首次主动使用它的时候才报告错误,如果这个类一直没有被主动使用,则一直不会报错。

    2. 类的加载过程:

    首先,我们都知道类的加载是由类加载器完成的,JVM中加载器分为三类:Bootstrap、Extension、App,其中BootStap类加载器是基于JVM的也就是说不同类型的JVM不一样,可以理解成native;Extension和Application类加载器继承自ClassLoader类是Java代码编写的。每类ClassLoader负责加载不同位置的class

    • Bootstrap ClassLoader(爷爷):jre\lib\rt.jar
      我们知道Java JDK包基本都在rt.jar中,我们可以看到常用的java.lang,java.util都在里面:


      rt.jar
    • Extension ClassLoader(父亲): jre\lib\ext\*.jar
    • App ClassLoader(儿子):它是用来加载在我们环境变量下的class文件。
      这里我们介绍了JVM自带的ClassLoader,最后通过ClassLoader源码来看看类加载器之间是如何工作的。
      前面说了Extension和App类加载器都继承自Classloader.java。在ClassLoader的源码注释中,很好的解释了ClassLoader这个类是做什么工作:

    A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.Every Class object contains a reference to the ClassLoader that defined it.

    译:类加载器是用来加载类文件的一个对象。ClassLoader这个class本身是一个抽象类,ClassLoader可以根据提供的类名定位到对应的类文件,最典型的方式就是将类名转换成文件名。每一个Class对象都可以访问到生成它的类加载器。

    这里的Class对象不就是我们说的加载的最终成果么!!!那么ClassLoader如何工作呢:

    The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.

    译:类加载器采用委派模型(即:双亲委派模型)来搜寻类或者资源。每一个类加载器实例都有一个与之关联的父亲加载器。当JVM要求子加载器去找某类或者资源时,子加载器会先委派给父亲加载器,直到顶层的根加载器(BootStrap加载器)
    接上面的说,所有的加载任务都会先一级级的传到BootStrap,如果它没找到(类加载器会到自己负责的文件目录中寻找)它会告诉子加载器,子加载器才会开始尝试寻找。它的整个流程如下:


    双亲委派机制

    在ClassLoader.java中有三个重要方法,对于继承它的类加载器需要去实现:

    Class findClass(String name)
    Class<?> loadClass
    final Class<?> defineClass(String name, byte[] b, int off, int len)
    

    loadClass就是双亲委派的实际过程,我们不妨看看源码:

       protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 首先判断这个name的class是否加载过,如果加载过c!=null,直接返回
                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
                    }
                    //此时c==null,表示父加载器也没有找到Class,这时候就自己找啰
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        //父加载器没找到Class,只能自己找,调用自己的findClass()
                        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;
            }
        }
    

    可以看到双亲委派的流程是loadClass控制的,真实的去磁盘上找文件是由findClass()方法执行,但是如果看到findClass定义,你可以发现其中没有内容,这就是各个级别的类加载器需要自己复写的方法。我们在findClass中搜索文件你,如果找到文件,则通过defineClass将文件转化成输入流,进而读到内存方法区中,返回一个Class对象。

    以上就是JVM对类加载流程的第一步:加载的完整过程及原理,通过类加载器,磁盘上的class文件就存到了内存方法区当中,下一步就是进行连接和初始化啦!

    相关文章

      网友评论

          本文标题:白话类加载

          本文链接:https://www.haomeiwen.com/subject/qlliictx.html