Java 文档 - java.lang.ClassLoader (Part 3)

177 阅读22分钟

9. 关于命名空间

我觉得讲类加载器,还是很有必要知道命名空间这个概念!实际上类加载器的一个必不可少的前提就是命名空间!

命名空间概念:

每个类加载器都有自己的命名空间,

命名空间由该加载器及所有父加载器所加载的类组成。

特别注意:

在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。

在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
由子加载器加载的类能看见父加载器的类,由父亲加载器加载的类不能看见子加载器加载的类

我们已经知道每个类只能被加载一次,其实这样说是不够准确的,怎样才算是准确的呢?那就涉及到命名空间的概念了!只有在相同的命名空间中,每个类才只能被加载一次,反过来说就是一个类在不同的命名空间中是可以被加载多次的,而被加载多次的Class对象是互相独立的!

9.1、如何理解

当然,直接把命名空间的概念直接抛给大家,如果没有接触过,100%是看不懂其中的含义的,我敢打包票,假一赔100万。。。那么博主就举出写例子让各位深刻体会一下!当然这些例子涉及自定义加载器的一些知识,建议先对自定义加载器有一定了解在看!

例子必知前提:
1、 自己在idea或者eclipse中创建的工程项目中只要编译之后都会有对应的class文件成在classPath目录中
2、 而这些目录是由ApplicationClassLoader应用加载器加载
3、 我之后会将class文件放到系统桌面地址上,而这些系统地址由自定义加载器指定,所以由自定义加载器加载

9.2 准备

事先编译好,然后将项目工程中的两个字节码class文件【File1和File2】拷贝到系统桌面路径上,编译main方法就会出现在项目工程(ClassPath)下,注意以下例子情况中系统桌面路径的class文件一直都存在!

Main方法情况:

  • 1、创建一个自定义加载器classloader2,并声明桌面class文件路径,接着加载File1

  • 2、打印File1的加载器

  • 3、newInstanceFile1的实例

File1类的方法情况:

  • 1、File1的构造方法中存在一行代码:new File2new实例代码

File2类的方法情况:

  • 1、打印File2的加载器

9.3 测试代码情景一

删除File1File2项目工程中的class文件,工程项目的两个class文件都删除(只存在系统桌面路径下的class文件)

结果:File1File2的加载器都是自定义加载器

9.4 测试代码情景二

只删除File1项目工程中的class文件

结果:File1的加载器是自定义加载器,而执行到File2实例的加载器是App应用加载器

9.5 测试代码情景三

只删除File2项目工程中的class文件

结果:File1的加载器都是APP应用加载器,而执行到File2实例的时候报NoClassDefFoundError异常

得出结论:加载一个类(File1)的时候,这个类里面调用了其他的类(File2)或者其他类方法的初始化代码,那么这里面的类也会试着从这个类的加载器开始向上委托加载,如果全都加载不了加载不了就报NoClassDefFoundError异常

当然这样理解命名空间和类加载机制还是远远不够的!

File2类中发生改变情况如下:

  • 1、File1的构造方法中存在一行new File2的实例这没变
  • 2、在File2的构造方法中,打印(访问)File1的class文件

9.6 测试代码情景四

只删除项目工程中File1的class文件

结果:File1的加载器都是自定义加载器,而执行到File2实例的加载器是App应用加载器,当运行到File2构造方法中的打印(访问)File1的class文件的时候报NoClassDefFoundError异常

得出结论:父亲加载器加载的类(File2)不能看见子加载器加载的类(File1

File1方法发生改变情况如下:
1、Main方法中newInstanceFile1的实例,File1的构造方法中存在一行new File2的实例这都没变
2、在File1的构造方法中,打印File2的class文件

9.7 测试代码情景五

只删除File1项目工程中的class文件

结果:File1的加载器都是自定义加载器,而执行到File2实例的加载器是App应用加载器,当运行到File1构造方法中的打印File2的class文件的时候没问题

得出结论:由子加载器加载的类(File1)能看见父加载器的类(File2)

当然还要注意知道的一点的是:

如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载类相互不可见。

当然整对上面的情况还是相当比较抽象,毕竟没上代码,如果有任何疑问,欢迎留言,宜春绝对第一时间回复!

10. JVM类加载机制

JVM的类加载机制主要有如下3种。

全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类,通俗讲就是儿子们都他么是懒猪,自己不管能不能做,就算能加载也先不干,先给自己的父亲做,一个一个往上抛,直到抛到启动类加载器也就是最顶级父类,只有父亲做不了的时候再没办法由下一个子类做,直到能某一个子类能做才做,之后的子类就直接返回,实力坑爹!

缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

11. 双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。也就是实力坑爹!

双亲委派机制:

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

3、如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException。

从代码层面了解几个Java中定义的类加载器及其双亲委派模式的实现,它们类图关系如下:

从图可以看出顶层的类加载器是抽象类ClassLoader类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),为了更好理解双亲委派模型,ClassLoader源码中的loadClass(String)方法该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。源码分析如下::

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          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
                  // 如果都没有找到,则通过自定义实现的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;
      }
  }

既然存在这个双亲委派模型,那么就一定有着存在的意义,其意义主要是:Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

双亲委派模型意义总结来讲就是:
1、系统类防止内存中出现多份同样的字节码
2、保证Java程序安全稳定运行

12. ClassLoader源码分析

ClassLoader类是一个抽象类,所有的类加载器都继承自ClassLoader(不包括启动类加载器),因此它显得格外重要,分析ClassLoader抽象类也是非常重要的!

简单小结一下ClassLoader抽象类中一些概念:

二进制概念(Binary name):格式如下

把二进制名字转换成文件名字,然后在文件系统中磁盘上读取其二进制文件(class文件),每一个class对象都包含了定义了这个类的classload对象,class类都是由类加载器加载的只有数组类型是有JVM根据需要动态生成。

特别注意数组类型

1、 数组类的类对象不是由类加载器创建的,而是根据Java运行时的需要自动创建的。
2、 数组类的类加载器getClassLoader()与它的元素类型的类加载器相同;如果元素类型是基本类型,则数组类没有类加载器也就是null,而这个null不同于根类加载器返回的null,它是单纯的null。

到这里,下面就主要分析ClassLoader抽象类中几个比较重要的方法。

12.1 loadClass

该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作:

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          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
                  // 如果都没有找到,则通过自定义实现的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()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass(“className”),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

12.2 findClass

在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下:

//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

12.3 defineClass(byte[] b, int off, int len)

defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
	  // 获取类的字节数组
      byte[] classData = getClassData(name);  
      if (classData == null) {
          throw new ClassNotFoundException();
      } else {
	      //使用defineClass生成class对象
          return defineClass(name, classData, 0, classData.length);
      }
  }

需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

12.4 resolveClass (Classc)

使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

12.5 ClassLoader小结

以上上述4个方法是ClassLoader类中的比较重要的方法,也是我们可能会经常用到的方法。接看SercureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联,前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能,在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

检查完父类加载器之后loadClass会去默认调用findClass方法,父类(ClassLoader)中的findClass方法主要是抛出一个异常。

findClass根据二进制名字找到对应的class文件,返回值为Class对象Class<?>

defineClass这个方法主要是将一个字节数组转换成Class实例,会抛三个异常,但只是threws一个,因为其他两个是运行时异常。

loadClass方法是一个加载一个指定名字的class文件,调用findLoadedClass (String)检查类是否已经加载…如果已经加装就不再加载而是直接返回第一次加载结果 所以一个类只会加载一次

13. 自定义类加载器

自定义核心目的是扩展java虚拟机的动态加载类的机制,JVM默认情况是使用双亲委托机制,虽然双亲委托机制很安全极高但是有些情况我们需要自己的一种方式加载,比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。因此自定义类加载器也是很有必要的。

自定义类加载器一般都是继承自 ClassLoader类,从上面对 loadClass方法来分析来看,我们只需要重写 findClass 方法即可。

自定义加载器中点:重写findClass,下面直接看自定义类加载器代码的流程:

package com.yichun.classloader;
import java.io.*;

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\dirtemp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.yichun.classloader.Demo1");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。上面代码程序只是简单Demo,并未对class文件进行加密,因此省略了解密的过程。这里有几点需要注意:

1、这里传递的文件名需要是类的全限定性名称,即com.yichun.test.classloading.Test格式的,因为
defineClass 方法是按这种格式进行处理的。

2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。

3、这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把com/yichun/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载。

14. 加载类的三种方式

到这里,相信大家已经对类的加载以及加载器有一定的了解了,那么你知道吗,其实加载类常见的有三种方式,如下:

1、静态加载,也就是通过new关键字来创建实例对象。

2、动态加载,也就是通过Class.forName()方法动态加载(反射加载类型),然后调用类的newInstance()方法实例化对象。

3、动态加载,通过类加载器loadClass()方法来加载类,然后调用类的newInstance()方法实例化对象

14.1 三种方式的区别:

1、第一种和第二种方式使用的类加载器是相同的,都是当前类加载器。(this.getClass.getClassLoader)。而3由用户指定类加载器。

2、如果需要在当前类路径以外寻找类,则只能采用第3种方式。

第3种方式加载的类与当前类分属不同的命名空间

3、第一种是静态加载,而第二、三种是动态加载。

14.2 两种异常(exception)

1、静态加载的时候如果在运行环境中找不到要初始化的类,抛出的是NoClassDefFoundError,它在JAVA的异常体系中是一个Error

2、动态态加载的时候如果在运行环境中找不到要初始化的类,抛出的是ClassNotFoundException,它在JAVA的异常体系中是一个checked异常

14.3 理解Class.forName

Class.forName()是一种获取Class对象的方法,而且是静态方法。

Class.forName()是一个静态方法,同样可以用来加载类,Class.forName()返回与给定的字符串名称相关联类或接口的Class对象。注意这是一种获取Class对象的方法

官方给出的API文档如下

publicstatic Class<?> forName(String className)

Returns the Class object associated withthe class or interface with the given string name. Invokingthis method is equivalent to:

Class.forName(className,true, currentLoader)

where currentLoader denotes the definingclass loader of the current class.

For example, thefollowing code fragment returns the runtime Class descriptor for theclass named java.lang.Thread:

Class t =Class.forName("java.lang.Thread")

A call to forName("X") causes theclass named X to beinitialized.

Parameters:

className - the fully qualifiedname of the desired class.

Returns:

the Class object for the classwith the specified name.

可以看出,Class.forName(className)实际上是调用Class.forName(className,true, this.getClass().getClassLoader())。第二个参数,是指Classloading后是不是必须被初始化。可以看出,使用Class.forName(className)加载类时则已初始化。

所以Class.forName()方法可以简单的理解为:获得字符串参数中指定的类,并初始化该类。

14.4 Class.forName与ClassLoader.loadClass区别

首先,我们必须先明确类加载机制的三个过程主要是:加载 --> 连接 --> 初始化。

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

  • Class.forName(name, initialize, loader):带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

这个时候,我们再来看一个程序:

package com.jvm.classloader;

class Demo{
    static {
        System.out.println("static 静态代码块");
    }
}

public class ClassLoaderDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader=ClassLoaderDemo.class.getClassLoader();
        //1、使用ClassLoader.loadClass()来加载类,不会执行初始化块
        classLoader.loadClass("com.jvm.classloader.Demo");
        
        //2、使用Class.forName()来加载类,默认会执行初始化块
        Class.forName("com.jvm.classloader.Demo");
        
        //3、使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
        Class.forName("com.jvm.classloader.Demo",false,classLoader);
    }
}

记得一个一个测试!我上面的程序是一次写了三个的并且已经标明了标号1、2、3!!!各位再自个电脑上跑一遍,思路就很会清晰了!

15. 总结

类的加载、连接与初始化:

1、加载:查找并加载类的二进制数据到java虚拟机中

2、 连接:

验证: 确保被加载的类的正确性

准备:

为类的静态变量分配内存,并将其初始化为默认值,但是到达初始化之前类变量都没有初始化为真正的初始值(如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。)
解析:把类中的符号引用转换为直接引用,就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程

3、初始化:为类的静态变量赋予正确的初始值

类从磁盘上加载到内存中要经历五个阶段:加载、连接、初始化、使用、卸载

Java程序对类的使用方式可分为两种

(1)主动使用
(2)被动使用

所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才能初始化他们
主动使用
(1)创建类的实例
(2)访问某个类或接口的静态变量 getstatic(助记符),或者对该静态变量赋值 putstatic
(3)调用类的静态方法 invokestatic
(4)反射(Class.forName(“com.test.Test”))
(5)初始化一个类的子类
(6)Java虚拟机启动时被标明启动类的类以及包含Main方法的类
(7)JDK1.7开始提供的动态语言支持(了解)

被动使用
除了上面七种情况外,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化

16. 特别注意

初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。

初始化类构造器:JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。

初始化对象构造器:JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。