Day18 类的加载器

Posted 扎心了,老铁

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day18 类的加载器相关的知识,希望对你有一定的参考价值。

  一个运行时的Java虚拟机(JVM)负责运行一个Java程序。

  当启动一个Java程序时,一个虚拟机实例诞生;当程序关闭退出,这个虚拟机实例也就随之消亡。

  如果在同一台计算机上同时运行多个Java程序,将得到多个Java虚拟机实例,每个Java程序都运行于它自己的Java虚拟机实例中。

 

  在如下几种情况下,Java虚拟机将结束生命周期:

  1.执行了System.exit()方法

  2.程序正常执行结束

  3.程序在执行过程中遇到了异常或错误而异常终止

  4.由于操作系统出现错误而导致Java虚拟机进程终止

类的生命周期

类的加载

xx.class的字节码文件,把字节码文件加载到方法区中

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

加载.class文件的方式

  1.从本地系统中直接加载

  2.通过网络下载.class文件

  3.从zip,jar等归档文件中加载.class文件

  4.从专有数据库中提取.class文件

  5.将Java源文件动态编译为.class文件

  类的加载的最终产品是位于堆区中的Class对象。

  Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

 

连接(验证、准备、解析)

连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

验证:验证字节码文件的准确性。

类的验证内容:

  1.类文件的结构检查

  确保类文件遵从Java类文件的固定格式。

  2.语义检查

  确保类本身符合Java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖。

  注意,语义检查的错误在编译器编译阶段就会通不过,但是如果有程序员通过非编译的手段生成了类文件,其中有可能会含有语义错误,此时的语义检查主要是防止这种没有编译而生成的class文件引入的错误。

  3.字节码验证

  确保字节码流可以被Java虚拟机安全地执行。

  字节码流代表Java方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。

  字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。

  4.二级制兼容性的验证

  确保相互引用的类之间的协调一致。

  例如,在Worker类的gotoWork()方法中会调用Car类的run()方法,Java虚拟机在验证Worker类时,会检查在方法区内是否存在Car类的run()方法,假如不存在(当Worker类和Car类的版本不兼容就会出现这种问题),就会抛出NoSuchMethodError错误。

准备:给类变量(静态变量)分配空间并且进行默认初始化。

在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。

  例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0

public class Sample {

    private static int a = 1;
    private static long b;

    static {
        b = 2;
    }
}

解析:把字节码中的一些符号转换成指针

在解析阶段,Java虚拟机会把类的二级制数据中的符号引用替换为直接引用。

  例如在Worker类的gotoWork()方法中会引用Car类的run()方法。

public void gotoWork() {

             car.run();// 这段代码在Worker类的二进制数据中表示为符号引用

      }

  在Worker类的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。

  在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置,这个指针就是直接引用

初始化

类变量(静态变量)进行声明处或静态块处初始化。

类的初始化步骤

  1.假如这个类还没有被加载和连接,那就先进行加载和连接

  2.假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类

  3.假如类中存在初始化语句,那就依次执行这些初始化语句。

类的初始化时机

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

  1.主动使用

  2.被动使用

  所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用才初始化它们。

主动使用的六种情况:

  1.创建类的实例。

  2.访问某个类或接口的静态变量,或者对该静态变量赋值。

  3.调用类的静态方法

  4.当使用反射方法强制创建某个类或接口的对象时

  5.当初始化某个子类时,该子类的所有父类都会被初始化

  6.当虚拟机Java命令运行启动类

除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化

使用

调用类中的方法

卸载

类的加载器

根类加载器

Bootstrap、ClassLoader

负责加载核心类库 lib下(C:\\Program Files\\Java\\jdk1.8.0_151\\jre\\lib),是用原生代码来实现的(C实现的),并不继承自java.lang.ClassLoader。

加载扩展类和应用程序类加载器,并指定它们的父类加载器。

扩展类加载器

Extension ClassLoader,父类是根类加载器

用来加载java的扩展库(JAVA_HOME/jre/lib/ext/*.jar,或java.ext.dirs路径下的内容)java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载java类。

由sun.miscLauncher$ExtClassLoader实现,继承自java.lang.ClassLoader

系统类加载器

System ClassLoader,父类是扩展类加载器

负责加载自己编写的类 classpath下

自定义类加载器

负责加载非classpath下的自己编写的类,父类是系统类加载器

public static void main(String[] args) {
        //AppClassLoader    系统类加载器
        System.out.println(TestLoader1.class.getClassLoader());
        //ExtClassLoader    扩张类加载器
        System.out.println(TestLoader1.class.getClassLoader().getParent());
        //null根类加载器无法查看
        System.out.println(TestLoader1.class.getClassLoader().getParent().getParent());
}

类加载的父委托机制

  loader2首先从自己的命名空间中查找Sample类是否已经被加载,如果已经加载,就直接返回代表Sample类的Class对象的引用。

  如果Sample类还没有被加载,loader2首先请求loader1代为加载,loader1再请求系统类加载器代为加载,系统类加载器再请求扩展类加载器代为加载,扩展类加载器再请求根类加载器代为加载。

  若根类加载器和扩展类加载器都不能加载,则系统类加载器尝试加载,若能加载成功,则将Sample类所对应的Class对象的引用返回给loader1,loader1再返回给loader2,从而成功将Sample类加载进虚拟机。

  若系统加载器不能加载Sample类,则loader1尝试加载Sample类,若loader1也不能成功加载,则loader2尝试加载。

  若所有的父加载器及loader2本身都不能加载,则抛出ClassNotFoundException异常。

  总结下来就是:

  每个加载器都优先尝试用父类加载,若父类不能加载则自己尝试加载;若成功则返回Class对象给子类,若失败则告诉子类让子类自己加载。所有都失败则抛出异常。

类加载器的工作原理

类加载器的工作原理基于三个机制:委托、可见性和单一性。

委托机制

当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。假设你有一个应用需要的类叫作Abc.class,首先加载这个类的请求由Application类加载器委托给它的父类加载器Extension类加载器,然后再委托给Bootstrap类加载器。Bootstrap类加载器会先看看rt.jar中有没有这个类,因为并没有这个类,所以这个请求由回到Extension类加载器,它会查看jre/lib/ext目录下有没有这个类,如果这个类被Extension类加载器找到了,那么它将被加载,而Application类加载器不会加载这个类;而如果这个类没有被Extension类加载器找到,那么再由Application类加载器从classpath中寻找。记住classpath定义的是类文件的加载目录,而PATH是定义的是可执行程序如javac,java等的执行路径。

可见性机制

根据可见性机制,子类加载器可以看到父类加载器加载的类,而反之则不行。所以下面的例子中,当Abc.class已经被Application类加载器加载过了,然后如果想要使用Extension类加载器加载这个类,将会抛出java.lang.ClassNotFoundException异常。

package test;
 
    import java.util.logging.Level;
    import java.util.logging.Logger;
 
    /**
     * Java program to demonstrate How ClassLoader works in Java,
     * in particular about visibility principle of ClassLoader.
     *
     * @author Javin Paul
     */
 
    public class ClassLoaderTest {
 
        public static void main(String args[]) {
            try {          
                //printing ClassLoader of this class
                System.out.println("ClassLoaderTest.getClass().getClassLoader() : "
                                     + ClassLoaderTest.class.getClassLoader());
 
                //trying to explicitly load this class again using Extension class loader
                Class.forName("test.ClassLoaderTest", true
                                ,  ClassLoaderTest.class.getClassLoader().getParent());
            } catch (ClassNotFoundException ex) {
                Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
 
    }

输出

ClassLoaderTest.getClass().getClassLoader() : sun.misc.Launcher$AppClassLoader@601bb1
16/08/2012 2:43:48 AM test.ClassLoaderTest main
SEVERE: null
java.lang.ClassNotFoundException: test.ClassLoaderTest
        at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at sun.misc.Launcher$ExtClassLoader.findClass(Launcher.java:229)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:247)
        at test.ClassLoaderTest.main(ClassLoaderTest.java:29)

单一性机制

根据这个机制,父加载器加载过的类不能被子加载器加载第二次。虽然重写违反委托和单一性机制的类加载器是可能的,但这样做并不可取。你写自己的类加载器的时候应该严格遵守这三条机制。

如何显式的加载类

Java提供了显式加载类的API:Class.forName(classname)和Class.forName(classname, initialized, classloader)。就像上面的例子中,你可以指定类加载器的名称以及要加载的类的名称。类的加载是通过调用java.lang.ClassLoader的loadClass()方法,而loadClass()方法则调用了findClass()方法来定位相应类的字节码。在这个例子中Extension类加载器使用了java.net.URLClassLoader,它从JAR和目录中进行查找类文件,所有以”/”结尾的查找路径被认为是目录。如果findClass()没有找到那么它会抛出java.lang.ClassNotFoundException异常,而如果找到的话则会调用defineClass()将字节码转化成类实例,然后返回。

什么地方使用类加载器

类加载器是个很强大的概念,很多地方被运用。最经典的例子就是AppletClassLoader,它被用来加载Applet使用的类,而Applets大部分是在网上使用,而非本地的操作系统使用。使用不同的类加载器,你可以从不同的源地址加载同一个类,它们被视为不同的类。J2EE使用多个类加载器加载不同地方的类,例如WAR文件由Web-app类加载器加载,而EJB-JAR中的类由另外的类加载器加载。有些服务器也支持热部署,这也由类加载器实现。你也可以使用类加载器来加载数据库或者其他持久层的数据。

 

以上是关于Day18 类的加载器的主要内容,如果未能解决你的问题,请参考以下文章

Day324.类加载子系统 -JVM

JVM day05 类加载阶段类加载器运行期优化

JVM day05 类加载阶段类加载器运行期优化

java自学之路-day21

Android 逆向类加载器 ClassLoader ( 类加载器源码简介 | BaseDexClassLoader | DexClassLoader | PathClassLoader )(代码片段

day21-反射&枚举